封面

依赖注入

Dependency Injection

原则、实践和模式

Principles, Practices, and Patterns

史蒂文·范德尔森

Steven van Deursen

马克西曼

Mark Seemann

曼宁BlackSized.png

曼宁

Manning

庇护岛

Shelter Island

简要内容

brief contents

  1. 第 1 部分:将依赖注入放在地图上
    1. 第 1 章:第一步
    2. 第 2 章:构建块
    3. 第 3 章:控制流程
  2. Part 1: Putting Dependency Injection on the map
    1. Chapter 1: First steps
    2. Chapter 2: Building blocks
    3. Chapter 3: Control flow
  3. 第 2 部分:目录
    1. 第 4 章:数据抽象
    2. 第 5 章:并发原语
    3. 第 6 章:通用服务器进程
  4. Part 2: Catalog
    1. Chapter 4: Data abstractions
    2. Chapter 5: Concurrency primitives
    3. Chapter 6: Generic server processes
  5. 第 3 部分:纯 DI
    1. 第 7 章:构建并发系统
    2. 第 8 章:容错基础知识
    3. 第 9 章:隔离错误影响
    4. 第 10 章:超越 GenServer
  6. Part 3: Pure DI
    1. Chapter 7: Building a concurrent system
    2. Chapter 8: Fault-tolerance basics
    3. Chapter 9: Isolating error effects
    4. Chapter 10: Beyond GenServer
  7. 第 4 部分:DI 容器
    1. 第 11 章:使用组件
    2. 第 12 章:构建分布式系统
    3. 第 13 章:运行系统
  8. Part 4: DI Containers
    1. Chapter 11: Working with components
    2. Chapter 12: Building a distributed system
    3. Chapter 13: Running the system

前言

preface

有一种与 Microsoft 相关的奇特现象,称为Microsoft Echo Chamber。Microsoft 是一个庞大的组织,而周围的 Microsoft 认证合作伙伴生态系统使该组织的规模成倍增长。如果你充分融入这个生态系统,就很难超越它的界限。每当您寻找 Microsoft 产品或技术问题的解决方案时,您很可能会找到涉及向其投入更多 Microsoft 产品的答案。无论您在回声室中大喊大叫,答案都是微软!

There’s a peculiar phenomenon related to Microsoft called the Microsoft Echo Chamber. Microsoft is a huge organization, and the surrounding ecosystem of Microsoft Certified Partners multiplies that size by orders of magnitude. If you’re sufficiently embedded in this ecosystem, it can be hard to see past its boundaries. Whenever you look for a solution to a problem with a Microsoft product or technology, you’re likely to find an answer that involves throwing even more Microsoft products at it. No matter what you yell within the echo chamber, the answer is Microsoft!

当 Microsoft 于 2003 年聘用我(马克)时,我已经牢牢地融入了回声室,为 Microsoft 认证合作伙伴工作了多年——我喜欢它!他们很快将我送往新奥尔良的内部技术会议,了解最新最好的 Microsoft 技术。

When Microsoft hired me (Mark) in 2003, I was already firmly embedded in the echo chamber, having worked for Microsoft Certified Partners for years—and I loved it! They soon shipped me off to an internal tech conference in New Orleans to learn about the latest and greatest Microsoft technology.

今天,我不记得我参加过的任何 Microsoft 产品会议,但我记得最后一天。那天,由于未能体验任何可以满足我对酷技术的渴望的课程,我主要是期待飞回丹麦的家。我的首要任务是找个地方坐下,这样我就可以处理我的电子邮件了,所以我选择了一个似乎与我无关的会议并启动了我的笔记本电脑。

Today, I can’t recall any of the Microsoft product sessions I attended—but I do remember the last day. On that day, having failed to experience any sessions that could satisfy my hunger for cool tech, I was mostly looking forward to flying home to Denmark. My top priority was to find a place to sit so I could attend to my email, so I chose a session that seemed marginally relevant for me and fired up my laptop.

会议结构松散,有几位主持人。其中一个是名叫 Martin Fowler 的大胡子,他谈到了测试驱动开发 (TDD) 和动态模拟。我从没听说过他,也没有仔细听,但我心里一定有什么印象。

The session was loosely structured and featured several presenters. One was a bearded guy named Martin Fowler, who talked about Test-Driven Development (TDD) and dynamic mocks. I had never heard of him, and I didn’t listen very closely, but something must have stuck in my mind.

回到丹麦后不久,我的任务是从头开始重写一个大型 ETL(提取、转换、加载)系统,我决定尝试一下 TDD(事实证明这是一个非常好的决定)。自然而然地使用了动态模拟,但也引入了管理依赖项的需要。我发现这是一个非常困难但又非常迷人的问题,我无法停止思考它。

Soon after returning to Denmark, I was tasked with rewriting a big ETL (extract, transform, load) system from scratch, and I decided to give TDD a try (it turned out to be a very good decision). The use of dynamic mocks followed naturally, but also introduced the need to manage dependencies. I found that to be a very difficult but very captivating problem, and I couldn’t stop thinking about it.

最初是我对 TDD 感兴趣的副作用,后来变成了一种热情。我做了很多研究,阅读了很多关于此事的博客文章,自己写了很多博客,试验了代码,并与任何愿意倾听的人讨论了这个话题。我越来越不得不在 Microsoft Echo Chamber 之外寻找灵感和指导。一路走来,人们将我与 ALT.NET 运动联系在一起,尽管我从来没有积极参与其中。我犯了所有可能犯的错误,但我逐渐能够对依赖注入 (DI) 形成连贯的理解。

What started as a side effect of my interest in TDD became a passion in itself. I did a lot of research, read lots of blog posts about the matter, wrote quite a few blogs myself, experimented with code, and discussed the topic with anyone who cared to listen. Increasingly, I had to look outside the Microsoft Echo Chamber for inspiration and guidance. Along the way, people associated me with the ALT.NET movement even though I was never very active in it. I made all the mistakes it was possible to make, but I was gradually able to develop a coherent understanding of Dependency Injection (DI).

当 Manning 向我提出关于 .NET 中的依赖注入的书的想法时,我的第一反应是,“这有必要吗?” 我觉得开发人员理解 DI 所需的所有概念已经在许多博客文章中进行了描述。有什么要补充的吗?老实说,我认为 .NET 中的 DI 是一个已经死了的主题。

When Manning approached me with the idea for a book about Dependency Injection in .NET, my first reaction was, “Is this even necessary?” I felt that all the concepts a developer needs to understand DI were already described in numerous blog posts. Was there anything to add? Honestly, I thought DI in .NET was a topic that had been done to death already.

然而,经过深思熟虑,我突然意识到,虽然知识肯定是存在的,但它非常分散并且使用了很多相互矛盾的术语。在本书第一版之前,没有关于 DI 的书名试图对它进行连贯的描述。进一步思考后,我意识到 Manning 为我提供了一个巨大的挑战和一个很好的机会来收集和系统化我对 DI 的所有了解。

Upon reflection, however, it dawned on me that while the knowledge is definitely out there, it’s very scattered and uses a lot of conflicting terminology. Before the first edition of this book, there were no titles about DI that attempted to present a coherent description of it. After thinking about it further, I realized that Manning was offering me a tremendous challenge and a great opportunity to collect and systematize all that I knew about DI.

结果就是这本书及其前身——第一版。它使用 .NET Core 和 C# 来介绍和描述 DI 的全面术语和指南,但我希望本书的价值能够超越平台。我认为这里阐述的模式语言是通用的。无论您是 .NET 开发人员还是使用其他面向对象的平台,我希望本书能帮助您成为更好的软件工程师。

The result is this book and its predecessor—the first edition. It uses .NET Core and C# to introduce and describe a comprehensive terminology and guidance for DI, but I hope the value of this book will reach well beyond the platform. I think the pattern language articulated here is universal. Whether you’re a .NET developer or use another object-oriented platform, I hope this book will help you be a better software engineer.

致谢

acknowledgments

感恩似乎是陈词滥调,但这只是因为它是人性的基本组成部分。在我们写这本书的过程中,许多人给了我们很好的感激理由,我们要感谢他们所有人。

Gratitude may seem like a cliché, but this is only because it’s such a fundamental part of human nature. While we were writing the book, many people gave us good reasons to be grateful, and we would like to thank them all.

首先,在业余时间写一本书让我们对这样的项目对婚姻和家庭生活的负担有多大有了新的认识。Mark的妻子Cecilie全程陪在身边,积极支持他。最重要的是,她明白这个项目对他来说有多重要。他们还在一起,马克期待能有更多时间陪伴她和他们的孩子 Linea 和 Jarl。史蒂文的妻子朱迪思给了他完成这项艰巨任务所需的空间,但她当然很高兴这个项目终于完成了。

First of all, writing a book in our spare time has given us a new understanding of just how taxing such a project is on marriage and family life. Mark’s wife Cecilie stayed with him and actively supported him during the whole process. Most significantly, she understood just how important this project was to him. They’re still together, and Mark looks forward to being able to spend more time with her and their kids Linea and Jarl. Steven’s wife Judith gave him the space needed to complete this immense undertaking, but she certainly is glad that the project is finally finished.

在更专业的层面上,我们要感谢曼宁给我们这个机会。Michael Stephens 发起了这个项目。Dan Maharry、Marina Michaels 和 Christina Taylor 担任我们的开发编辑,并密切关注文本的质量。他们帮助我们找出手稿中的弱点,并提供了广泛的建设性批评。

On a more professional level, we want to thank Manning for giving us this opportunity. Michael Stephens initiated the project. Dan Maharry, Marina Michaels, and Christina Taylor served as our development editors and kept a keen eye on the quality of the text. They helped us identify weak spots in the manuscript and provided extensive constructive criticism.

Karsten Strøbæk 担任我们的技术开发编辑,通读了许多早期草稿,并提供了很多有用的反馈。Mark 撰写第一版时 Karsten 就在场,并在当时担任制作期间的技术校对员。在这一版中,技术校对由 Chris Heneghan 完成,他在整个手稿中发现了许多细微的错误和不一致之处。

Karsten Strøbæk served as our technical development editor, read through numerous early drafts, and provided much helpful feedback. Karsten was there when Mark wrote the first edition and served as the technical proofreader during production at that time. In this edition, technical proofreading was done by Chris Heneghan, who caught many subtle bugs and inconsistencies throughout the manuscript.

写完稿子,我们就进入了制作环节。这是由 Anthony Calcara 管理的。在此过程中,Frances Buran 是我们的文字编辑,而 Nichole Beard 则密切关注本书的图形和图表。

After we were done writing the manuscript, we entered the production process. This was managed by Anthony Calcara. During that process, Frances Buran was our copyeditor, while Nichole Beard held a close watch on the book’s graphics and diagrams.

以下审稿人在不同的发展阶段阅读了手稿,我们感谢他们的评论和见解:Ajay Bhosale、Björn Nordblom、Cemre Mengu、Dennis Sellinger、Emanuele Origgi、Ernesto Cardenas Cangahuala、Gustavo Gomes、Igor Kochetov、Jeremy Caney 、贾斯汀·库尔斯顿、米克尔·阿伦托夫特、帕斯夸莱·齐尔波利、罗伯特·莫里森、塞尔吉奥·罗梅罗、肖恩·林和斯蒂芬·伯恩。本书的评论编辑 Ivan Martinovic 使评论成为可能。

The following reviewers read the manuscript at various stages of development, and we’re grateful for their comments and insight: Ajay Bhosale, Björn Nordblom, Cemre Mengu, Dennis Sellinger, Emanuele Origgi, Ernesto Cardenas Cangahuala, Gustavo Gomes, Igor Kochetov, Jeremy Caney, Justin Coulston, Mikkel Arentoft, Pasquale Zirpoli, Robert Morrison, Sergio Romero, Shawn Lam, and Stephen Byrne. Reviewing was made possible by Ivan Martinovic, the book’s review editor.

Manning 早期访问计划 (MEAP) 的许多参与者也提供了反馈并提出了暴露文本薄弱部分的难题。

Many of the participants in the Manning Early Access Program (MEAP) also provided feedback and asked difficult questions that exposed the weak parts of the text.

特别感谢 Jeremy Caney,他最初是 MEAP 参与者,后来被提升为审稿人。他为我们提供了大量的反馈,包括语言和上下文。他对 DI 和软件设计的深刻理解非常宝贵。

Special thanks go out to Jeremy Caney, who started out as a MEAP participant but was promoted to reviewer. He supplied us with an immense amount of feedback, both linguistic and contextual. His deep understanding of DI and software design was invaluable.

还要特别感谢 Ric Slappendel。Ric 建议我们如何使用 DI 编写 UWP 应用程序。他对 WPF、UWP 和 XAML 的了解为我们节省了无数的时间和不眠之夜,并完整地塑造了 7.2 节及其配套代码示例。如果没有 Ric 的帮助,我们很可能会写出一本根本不讨论 UWP 的书。

Also special thanks to Ric Slappendel. Ric advised us on how to compose UWP applications using DI. His knowledge about WPF, UWP, and XAML saved us countless hours and sleepless nights, and completely shaped section 7.2 and its companion code examples. Without Ric’s help, we likely would’ve ended up with a book that didn’t discuss UWP at all.

Alex Meyer-Gleaves 和 Travis Illig 审阅了第 13 章的早期版本,并向我们提供了有关使用新的 Autofac 配置和 Decorator 支持的反馈。我们感谢他们的参与。

Alex Meyer-Gleaves and Travis Illig reviewed early versions of chapter 13 and provided us with feedback on using the new Autofac configuration and Decorator support. We’re grateful for their participation.

最后,Mogens Heller Grabe 礼貌地允许我们使用他的吹风机图片,直接连接到墙上的插座。

And finally, Mogens Heller Grabe courteously allowed us to use his picture of a hair dryer wired directly into a wall outlet.

关于这本书

about this book

这是一本关于依赖注入(DI)的书,首先也是最重要的。它也是一本关于 .NET 的书,但它的重要性要小得多。尽管 C# 用于代码示例,但本书中的大部分讨论都可以轻松应用于其他语言和平台。事实上,我们通过阅读以 Java 或 C++ 为例的书籍,了解了很多底层原理和模式。

This is a book about Dependency Injection (DI), first and foremost. It’s also a book about .NET, but that’s much less important. Although C# is used for code examples, much of the discussion in this book can be easily applied to other languages and platforms. In fact, we learned a lot of the underlying principles and patterns from reading books where Java or C++ was used in examples.

DI 是一组相关的模式和原则。它是一种思考和设计代码的方式,而不仅仅是一种特定的技术。使用 DI 的最终目的是在面向对象的范例中创建可维护的软件。

DI is a set of related patterns and principles. It’s a way to think about and design code, more than it is a specific technology. The ultimate purpose of using DI is to create maintainable software within the object-oriented paradigm.

本书中使用的概念都与面向对象编程有关。DI 解决的问题(代码可维护性)是普遍的,但建议的解决方案是在静态类型语言的面向对象编程范围内给出的:C#、Java、Visual Basic .NET、C++ 等。您不能将 DI 应用于过程编程,它可能不是函数式或动态语言中的最佳解决方案。

The concepts used throughout this book all relate to object-oriented programming. The problem that DI addresses (code maintainability) is universal, but the proposed solution is given within the scope of object-oriented programming in statically typed languages: C#, Java, Visual Basic .NET, C++, and so on. You can’t apply DI to procedural programming, and it may not be the best solution in functional or dynamic languages.

孤立的 DI 只是一件小事,但它与面向对象软件设计的大量复杂原则和模式紧密交织在一起。尽管本书自始至终始终专注于 DI,但它还根据 DI 可以提供的特定视角讨论了许多其他主题。本书的目标不仅仅是教您有关 DI 的细节:目标是让您成为更好的面向对象的程序员。

DI in isolation is just a small thing, but it’s closely interwoven with a large complex of principles and patterns for object-oriented software design. Whereas the book focuses consistently on DI from start to finish, it also discusses many of these other topics in the light of the specific perspective that DI can give. The goal of the book is more than just teaching you about DI specifics: the goal is to make you a better object-oriented programmer.

谁应该读这本书?

Who should read this book?

人们很想说这是一本面向所有 .NET 开发人员的书。但今天的 .NET 社区非常庞大,涵盖了使用 Web 应用程序、桌面应用程序、智能手机、RIA、集成、办公自动化、内容管理系统甚至游戏的开发人员。尽管 .NET 是面向对象的,但并非所有开发人员都编写面向对象的代码。

It would be tempting to state that this is a book for all .NET developers. But the .NET community today is vast and spans developers working with web applications, desktop applications, smartphones, RIA, integration, office automation, content management systems, and even games. Although .NET is object oriented, not all of those developers write object-oriented code.

这是一本关于面向对象编程的书,因此读者至少应该对面向对象感兴趣并了解什么是接口。几年的设计模式或SOLID原则的专业经验和知识当然也会大有裨益。事实上,我们并不期望初学者能从本书中学到很多东西;它主要针对有经验的开发人员和软件架构师。

This is a book about object-oriented programming, so at a minimum readers should be interested in object orientation and understand what an interface is. A few years of professional experience and knowledge of design patterns or SOLID principles will certainly be of benefit as well. In fact, we don’t expect beginners to get much out of the book; it’s mostly targeted toward experienced developers and software architects.

这些示例都是用 C# 编写的,因此使用其他 .NET 语言的读者必须能够阅读和理解 C#。熟悉 Java 和 C++ 等非 .NET 面向对象语言的读者也可能会发现这本书很有价值,因为特定于 .NET 平台的内容相对较少。就个人而言,我们阅读了很多带有 Java 示例的模式书籍,仍然从中获益良多,所以我们希望反之亦然。

The examples are all written in C#, so readers working with other .NET languages must be able to read and understand C#. Readers familiar with non-.NET object-oriented languages like Java and C++ may also find the book valuable, because the .NET platform-specific content is relatively light. Personally, we read a lot of pattern books with examples in Java and still get a lot out of them, so we hope the converse is true as well.

路线图

Roadmap

本书内容分为四个部分。理想情况下,我们希望您首先从头到尾阅读它,然后将其用作参考,但我们理解您是否有其他优先事项。出于这个原因,编写了大部分章节,以便您可以直接深入并从这一点开始阅读。

The contents of this book are divided into four parts. Ideally, we’d like you to first read it from cover to cover and then subsequently use it as a reference, but we understand if you have other priorities. For that reason, a majority of the chapters are written so that you can dive right in and start reading from that point.

第一部分是主要例外。它包含对 DI 的一般介绍,最好按顺序阅读。第二部分是模式等的目录,而第三部分也是最大的部分是从三个不同角度检查 DI。本书的第四部分是三个DI Container库的目录。

The first part is the major exception. It contains a general introduction to DI and is probably best read sequentially. The second part is a catalog of patterns and the like, whereas the third and largest part is an examination of DI from three different angles. The fourth part of the book is a catalog of three DI Container libraries.

有很多相互关联的概念,因为我们第一次介绍它们时感觉很自然,这意味着我们经常在正式介绍概念之前就提及它们。为了将这些通用概念与更多本地术语区分开来,我们始终使用小型大写字母来使它们脱颖而出。所有这些术语都在词汇表中进行了简要定义,其中还包含对更广泛描述的引用。

There are a lot of interconnected concepts, and, because we introduce them the first time it feels natural, this means we often mention concepts before we’ve formally introduced them. To distinguish these universal concepts from more local terms, we consistently use Small Caps to make them stand out. All these terms are briefly defined in the glossary, which also contains references to a more extensive description.

第 1 部分是对 DI 的一般介绍。如果您不知道什么是 DI,请从这里开始;但即使你这样做了,你也可能想要熟悉第 1 部分的内容,因为它建立了本书其余部分使用的大量上下文和术语。第 1 章讨论了 DI 的目的和好处,并提供了一个大纲。第 2 章包含一个大型且相当全面的紧耦合代码示例,第 3 章解释了如何使用 DI 重新实现相同的示例。与其他部分相比,第 1 部分的内容更为线性。您需要从头开始阅读每一章,以便从中获得最大收益。

Part 1 is a general introduction to DI. If you don’t know what DI is, this is the place to start; but even if you do, you may want to familiarize yourself with the contents of part 1, as it establishes a lot of the context and terminology used in the rest of the book. Chapter 1 discusses the purpose and benefits of DI and provides a general outline. Chapter 2 contains a big and rather comprehensive example of tightly coupled code, and chapter 3 explains how to reimplement the same example using DI. Compared to the other parts, part 1 has a more linear progression of its content. You’ll need to read each chapter from the beginning to gain the most from it.

第 2 部分是模式、反模式和代码味道的目录。在这里您可以找到关于如何实施 DI 和需要注意的危险的规范性指导。第 4 章是 DI 设计模式目录,相反,第 5 章是反模式目录。第 6 章包含常见问题的通用解决方案。作为一个目录,每一章都包含一组松散相关的部分,这些部分旨在单独阅读以及按顺序阅读。

Part 2 is a catalog of patterns, anti-patterns, and code smells. This is where you’ll find prescriptive guidance on how to implement DI and the dangers to look out for. Chapter 4 is a catalog of DI design patterns, and, conversely, chapter 5 is a catalog of anti-patterns. Chapter 6 contains generalized solutions to commonly occurring issues. As a catalog, each chapter contains a set of loosely related sections that are designed to be read in isolation as well as in sequence.

第 3 部分从三个不同的角度检查 DI:对象组合生命周期管理拦截。在第 7 章中,我们讨论了如何在现有应用程序框架(ASP.NET Core 和 UWP)之上实现 DI,以及如何使用控制台应用程序实现 DI。第 8 章描述了如何管理依赖生命周期以避免资源泄漏。虽然结构上没有前几章那么严格,但该章的大部分内容都可以作为知名生活方式的目录。其余三章描述了如何使用Cross-Cutting Concerns组合应用程序。第 9 章介绍拦截的基础知识使用装饰器,而第 10 章和第 11 章深入探讨了面向方面编程的概念。这是您从之前的所有工作中获益的地方,因此,在许多方面,我们认为这是本书的高潮。

Part 3 examines DI from three different angles: Object Composition, Lifetime Management, and Interception. In chapter 7, we discuss how to implement DI on top of existing application frameworks—ASP.NET Core and UWP—and how to implement DI using a console application. Chapter 8 describes how to manage Dependency lifetimes to avoid resources leaks. Whereas the structure is a little less stringent than previous chapters, a large part of that chapter can be used as a catalog of well-known Lifestyles. The remaining three chapters describe how to compose applications with Cross-Cutting Concerns. Chapter 9 goes into the basics of Interception using Decorators, whereas chapters 10 and 11 dive deep into the concept of Aspect-Oriented Programming. This is where you harvest the benefits of all the work that came before, so, in many ways, we consider this to be the climax of the book.

第 4 部分是DI 容器库的目录。它首先讨论什么是DI 容器以及它们如何融入整体情况。其余三章分别详细介绍了一个特定的容器:Autofac、Simple Injector 和 Microsoft.Extensions.DependencyInjection。为了节省空间,每一章都以相当浓缩的形式介绍其容器,因此您可能只想阅读您最感兴趣的一两个容器。在许多方面,我们将这三章视为一组非常庞大的附录。

Part 4 is a catalog of DI Container libraries. It starts with a discussion on what DI Containers are and how they fit into the overall picture. The remaining three chapters each cover a specific container in a fair amount of detail: Autofac, Simple Injector, and Microsoft.Extensions.DependencyInjection. Each chapter covers its container in a rather condensed form to save space, so you may want to read about only the one or two containers that interest you the most. In many ways, we regard these three chapters as a very big set of appendixes.

为了使 DI 原则和模式的讨论不涉及任何特定的容器 API,本书的大部分内容(第 4 部分除外)都没有引用特定的容器。这也是容器在第 4 部分中以如此强大的力量出现的原因。我们希望通过保持一般性的讨论,本书将在更长的时间内有用。

To keep the discussion of DI principles and patterns free of any specific container APIs, most of the book, with the exception of part 4, is written without referencing a particular container. This is also why the containers appear with such force in part 4. It’s our hope that by keeping the discussion general, the book will be useful for a longer period of time.

您还可以获取第 1 部分到第 3 部分中的概念,并将它们应用到第 4 部分中未涵盖的容器库。不幸的是,我们无法涵盖一些可用的好容器。但即使对于这些库的用户,我们也希望本书能提供很多帮助。

You can also take the concepts from parts 1 through 3 and apply them to container libraries not covered in part 4. There are good containers available that, unfortunately, we couldn’t cover. But even for users of these libraries, we hope that this book has a lot to offer.

代码约定和下载

Code conventions and downloads

本书中有很多代码示例。其中大部分使用 C#,但也有一些 XML 和 JSON。列表和文本中的源代码与 fixed-width font like this普通文本分开。

There are many code examples in this book. Most of those are in C#, but there’s also a bit of XML and JSON here and there. Source code in listings and text is in a fixed-width font like this to separate it from ordinary text.

本书的所有源代码均使用 C# 和 Visual Studio 2017 编写。ASP.NET Core 应用程序是针对 ASP.NET Core v2.1 编写的。

All the source code for the book is written in C# and Visual Studio 2017. The ASP.NET Core applications are written against ASP.NET Core v2.1.

本书中描述的技术中只有少数依赖于现代语言的特性。我们希望在保守和现代编码风格之间取得合理的平衡。当我们专业地编写代码时,我们会在更大程度上使用现代语言功能,但在大多数情况下,最高级的功能是泛型和 LINQ。我们最不想让你明白 DI 只能应用于超现代语言。

Only a few of the techniques described in this book hinge on modern language features. We wanted to strike a reasonable balance between conservative and modern coding styles. When we write code professionally, we use modern language features to a far greater degree, but, for the most part, the most advanced features are generics and LINQ. The last thing we want is for you to get the idea that DI can only be applied with ultra-modern languages.

为一本书编写代码示例会带来一系列挑战。与现代计算机显示器相比,一本书只允许非常短的代码行。用简短但隐晦的方法和变量名称编写简洁风格的代码是非常诱人的。即使您附近有 IDE 和调试器,这样的代码也已经很难理解为真正的代码,但在书本中很难理解。我们发现尽可能保持名称的可读性非常重要。为了使一切都合适,我们有时不得不求助于一些非正统的换行符。所有代码都可以编译,但有时格式看起来有点滑稽。

Writing code examples for a book presents its own set of challenges. Compared to a modern computer monitor, a book only allows for very short lines of code. It was very tempting to write code in a terse style with short but cryptic names for methods and variables. Such code is already difficult to understand as real code even when you have an IDE and a debugger nearby, but it becomes really difficult to follow in a book. We found it very important to keep names as readable as possible. To make it all fit, we’ve sometimes had to resort to some unorthodox line breaks. All the code compiles, but sometimes the formatting looks a bit funny.

该代码还使用了 C#var关键字。在我们的专业代码中,线宽不受书页大小的限制,我们在应用时经常使用不同的编码风格var。在这里,为了节省空间,我们var在判断显式声明使代码可读性降低时使用 whenever。

The code also makes use of the C# var keyword. In our professional code, where line width isn’t limited by the size of a book’s page, we often use a different coding style when applying var. Here, to save space, we use var whenever we judge that an explicit declaration makes the code less readable.

这个词通常用作类型的同义词。在.NET中,类、结构、接口、枚举等都是类型,但由于类型这个词在普通语言中也是一个含义重载很多的词,如果使用它往往会使文本不太清晰。

The word class is often used as a synonym for a type. In .NET, classes, structs, interfaces, enums, and so on are all types, but because the word type is also a word with a lot of overloaded meaning in ordinary language, it would often make the text less clear if used.

本书中的大部分代码都与贯穿全书的一个总体示例相关:一个带有支持性内部管理应用程序的在线商店。这是您可以在任何软件文本中看到的最不令人兴奋的示例,但我们选择它有以下几个原因:

Most of the code in this book relates to an overarching example running through the book: an online store complete with supporting internal management applications. This is about the least exciting example you can expect to see in any software text, but we chose it for a few reasons:

  • 对于大多数读者来说,这是一个众所周知的问题领域。虽然这看起来很无聊,但我们认为这是一个优势,因为它不会从 DI 中抢走焦点。
  • It’s a well-known problem domain for most readers. Although it may seem boring, we think this is an advantage, because it doesn’t steal focus from DI.
  • 我们还必须承认,我们真的想不出任何其他领域足以支持我们想到的所有不同场景。
  • We also have to admit that we couldn’t really think of any other domain that was rich enough to support all the different scenarios we had in mind.

我们编写了大量代码来支持代码示例,其中大部分代码不在本书中。事实上,我们几乎使用测试驱动开发 (TDD) 编写了所有内容,但由于这不是一本 TDD 书籍,因此我们通常不会在书中展示单元测试。

We wrote a lot of code to support the code examples, and most of that code isn’t in this book. In fact, we wrote almost all of it using Test-Driven Development (TDD), but as this isn’t a TDD book, we generally don’t show the unit tests in the book.

本书中所有示例的源代码都可以从 Manning 的网站获得:www.manning.com/books/dependency-injection-principles-practices-patterns。下载根目录中的 README.md 包含编译和运行代码的说明。

The source code for all examples in this book is available from Manning’s website: www.manning.com/books/dependency-injection-principles-practices-patterns. The README.md in the root of the download contains instructions for compiling and running the code.

liveBook 论坛

liveBook discussion forum

购买依赖注入原则、实践和模式,包括免费访问由 Manning Publications 运营的私人网络论坛,您可以在其中对本书发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛并订阅它,请将 Web 浏览器指向https://livebook.manning.com/#!/book/dependency-injection-principles-practices-patterns/discussion您还可以在https://livebook.manning.com/#!/discussion上了解有关 Manning 论坛和行为规则的更多信息。

The purchase of Dependency Injection Principles, Practices, and Patterns, includes free access to a private web forum run by Manning Publications, where you can make comments about the book, ask technical questions, and receive help from the authors and from other users. To access the forum and subscribe to it, point your web browser to https://livebook.manning.com/#!/book/dependency-injection-principles-practices-patterns/discussion. You can also learn more about Manning’s forums and the rules of conduct at https://livebook.manning.com/#!/discussion.

Manning 对我们的读者的承诺是提供一个场所,让各个读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与任何特定数量的承诺,他们对论坛的贡献仍然是自愿的(并且是无偿的)。我们建议您问他们一些具有挑战性的问题,以免他们的兴趣发生偏差!只要本书还在印刷,就可以从出版商的网站访问图书论坛和以前讨论的档案。

Manning’s commitment to our readers is to provide a venue where a meaningful dialogue between individual readers and between readers and the authors can take place. It isn’t a commitment to any specific amount of participation on the part of the authors, whose contribution to the forum remains voluntary (and unpaid). We suggest that you ask them some challenging questions lest their interest stray! The book forum and the archives of previous discussions will be accessible from the publisher’s website as long as the book is in print.

关于作者

about the authors

Steven van Deursen 是一位荷兰自由职业者 .NET 开发人员和架构师,自 2002 年以来一直在该领域工作。他住在奈梅亨,喜欢为乐趣和利润而编写代码。除了编写代码之外,Steven 还练习武术,喜欢外出就餐,当然也喜欢上一杯威士忌。

Steven van Deursen is a Dutch freelance .NET developer and architect with experience in the field since 2002. He lives in Nijmegen and enjoys writing code for fun and profit. Besides writing code, Steven trains in martial arts, likes to go out for food, and certainly fancies a good whiskey.

Mark Seemann 是一名程序员、软件架构师和演讲者,居住在丹麦哥本哈根。他从 1995 年开始从事软件工作,从 2003 年开始从事 TDD,包括在 Microsoft 工作六年,担任顾问、开发人员和架构师。Mark 目前专业从事软件开发,在哥本哈根工作。他喜欢阅读、绘画、弹吉他、美酒和美食。

Mark Seemann is a programmer, software architect, and speaker living in Copenhagen, Denmark. He has been working with software since 1995 and TDD since 2003, including six years with Microsoft as a consultant, developer, and architect. Mark is currently professionally engaged with software development and is working out of Copenhagen. He enjoys reading, painting, playing the guitar, good wine, and gourmet food.

关于封面插画

about the cover illustration

依赖注入原则、实践和模式的封面上是“来自 Vodnjan 的女人”,Vodnjan 是克罗地亚附近亚得里亚海伊斯特拉半岛内陆的一个小镇。该插图取自 Nikola Arsenovic 于 2003 年在克罗地亚斯普利特的民族志博物馆出版的 19 世纪中叶克罗地亚传统服饰相册的复制品。斯普利特本身位于中世纪城镇中心的罗马核心:公元 304 年左右的戴克里先皇帝退休宫殿的废墟。这本书包括克罗地亚不同地区人物的精美彩色插图,并附有服饰和日常生活。Vodnjan 是一个具有重要文化和历史意义的城镇,坐落在山顶上,可以欣赏到亚得里亚海的美丽景色,并以其众多的教堂和神圣艺术珍品而闻名。封面上的女人身穿黑色亚麻长裙,白色亚麻衬衫外罩黑色短夹克。这件夹克饰有蓝色刺绣,蓝色亚麻围裙使服装更加完美。女人还戴着一顶大沿黑帽,一条花围巾,戴着大圈形耳环。她优雅的服装表明她是城镇居民,而不是村庄居民。周边乡村的民间服饰色彩更艳丽,以羊毛为原料,饰有丰富的刺绣。女人还戴着一顶大沿黑帽,一条花围巾,戴着大圈形耳环。她优雅的服装表明她是城镇居民,而不是村庄居民。周边乡村的民间服饰色彩更艳丽,以羊毛为原料,饰有丰富的刺绣。女人还戴着一顶大沿黑帽,一条花围巾,戴着大圈形耳环。她优雅的服装表明她是城镇居民,而不是村庄居民。周边乡村的民间服饰色彩更艳丽,以羊毛为原料,饰有丰富的刺绣。

On the cover of Dependency Injection Principles, Practices, and Patterns is “A woman from Vodnjan,” a small town in the interior of the peninsula of Istria in the Adriatic Sea, off Croatia. The illustration is taken from a reproduction of an album of Croatian traditional costumes from the mid-nineteenth century by Nikola Arsenovic, published by the Ethnographic Museum in Split, Croatia, in 2003. The illustrations were obtained from a helpful librarian at the Ethnographic Museum in Split, itself situated in the Roman core of the medieval center of the town: the ruins of Emperor Diocletian’s retirement palace from around AD 304. The book includes finely colored illustrations of figures from different regions of Croatia, accompanied by descriptions of the costumes and of everyday life. Vodnjan is a culturally and historically significant town, situated on a hilltop with a beautiful view of the Adriatic and known for its many churches and treasures of sacral art. The woman on the cover wears a long, black linen skirt and a short, black jacket over a white linen shirt. The jacket is trimmed with blue embroidery, and a blue linen apron completes the costume. The woman is also wearing a large-brimmed black hat, a flowered scarf, and big hoop earrings. Her elegant costume indicates that she is an inhabitant of the town, rather than a village. Folk costumes in the surrounding countryside are more colorful, made of wool, and decorated with rich embroidery.

在过去的 200 年里,着装规范和生活方式发生了变化,当时如此丰富的地区多样性已经消失。现在很难区分不同大陆的居民,更不用说仅相隔几英里的不同小村庄或城镇了。也许我们已经用文化多样性换取了更加多样化的个人生活——当然是为了更加多样化和快节奏的技术生活。

Dress codes and lifestyles have changed over the last 200 years, and the diversity by region, so rich at the time, has faded away. It is now hard to tell apart the inhabitants of different continents, let alone of different hamlets or towns separated by only a few miles. Perhaps we have traded cultural diversity for a more varied personal life—certainly for a more varied and fast-paced technological life.

Manning 以两个世纪前地区生活的丰富多样性为基础,通过书籍封面来庆祝计算机行业的创造力和主动性,并通过像这本书这样的旧书和收藏中的插图将其带回生活。

Manning celebrates the inventiveness and initiative of the computer business with book covers based on the rich diversity of regional life of two centuries ago, brought back to life by illustrations from old books and collections like this one.

第 1 部分

将依赖注入放在地图上

Part 1

Putting Dependency Injection on the map

依赖注入 (DI) 是面向对象编程中最容易被误解的概念之一。混淆是丰富的,跨越术语、目的和机制。它应该被称为依赖注入、依赖反转、控制反转,还是第三方连接?DI 的目的仅仅是为了支持单元测试,还是有更广泛的目的?DI 与服务位置相同吗?我们是否需要DI 容器来应用 DI?

Dependency Injection (DI) is one of the most misunderstood concepts of object-oriented programming. The confusion is abundant and spans terminology, purpose, and mechanics. Should it be called Dependency Injection, Dependency Inversion, Inversion of Control, or even Third-Party Connect? Is the purpose of DI only to support unit testing, or is there a broader purpose? Is DI the same as Service Location? Do we need DI Containers to apply DI?

有很多讨论 DI 的博客文章、杂志文章、会议报告等,但不幸的是,其中许多使用了相互矛盾的术语或给出了错误的建议。整个行业都是如此,甚至像微软这样有影响力的大公司也加剧了混乱。

There are plenty of blog posts, magazine articles, conference presentations, and so on that discuss DI, but, unfortunately, many of them use conflicting terminology or give bad advice. This is true across the board, and even big and influential actors like Microsoft add to the confusion.

它不必是这样的。在本书中,我们呈现并使用一致的术语。在大多数情况下,我们采用并阐明了其他人定义的现有术语,但偶尔也会添加一些以前不存在的术语。这极大地帮助我们发展了 DI 范围或边界的规范。

It doesn’t have to be this way. In this book, we present and use a consistent terminology. For the most part, we’ve adopted and clarified existing terminology defined by others, but, occasionally, we add a bit of terminology where none existed previously. This has helped us tremendously in evolving a specification of the scope or boundaries of DI.

所有不一致和糟糕建议背后的根本原因之一是 DI 的界限非常模糊。DI 在哪里结束,其他面向对象的概念从哪里开始?我们认为不可能在 DI 和编写好的面向对象代码的其他方面之间划清界限。要谈论 DI,我们必须引入其他概念,例如SOLID、Clean Code,甚至Aspect-Oriented Programming。我们不认为我们可以在不涉及其他一些主题的情况下可靠地撰写有关 DI 的文章。

One of the underlying reasons behind all the inconsistency and bad advice is that the boundaries of DI are quite blurry. Where does DI end, and where do other object-oriented concepts begin? We think that it’s impossible to draw a distinct line between DI and other aspects of writing good object-oriented code. To talk about DI, we have to pull in other concepts such as SOLID, Clean Code, and even Aspect-Oriented Programming. We don’t feel that we can credibly write about DI without also touching on some of these other topics.

本书的第一部分帮助您了解 DI 相对于软件工程其他方面的位置——可以说,将其放在地图上。第 1 章让您快速浏览 DI,涵盖其目的、原则和好处,以及提供本书其余部分范围的大纲。它着眼于大局,并没有涉及很多细节。如果您想了解什么是 DI 以及为什么您应该对它感兴趣,那么这里就是您的起点。本章假设您之前没有 DI 知识。即使您已经了解 DI,您可能仍想阅读它——结果可能与您的预期不同。

The first part of the book helps you understand the place of DI in relation to other facets of software engineering — putting it on the map, so to speak. Chapter 1 gives you a quick tour of DI, covering its purpose, principles, and benefits, as well as providing an outline of the scope for the rest of the book. It’s focused on the big picture and doesn’t go into a lot of details. If you want to learn what DI is and why you should be interested in it, this is the place to start. This chapter assumes you have no prior knowledge of DI. Even if you already know about DI, you may still want to read it — it may turn out to be something other than what you expected.

另一方面,第 2 章和第 3 章完全留给一个大例子。此示例旨在让您对 DI 有更具体的感受。为了将 DI 与更传统的编程风格进行对比,第 2 章展示了示例电子商务应用程序的典型、紧密耦合的实现。第 3 章随后用 DI 重新实现它。

Chapters 2 and 3, on the other hand, are completely reserved for one big example. This example is intended to give you a much more concrete feel for DI. To contrast DI with a more traditional style of programming, chapter 2 showcases a typical, tightly coupled implementation of a sample e-commerce application. Chapter 3 then subsequently reimplements it with DI.

在这一部分中,我们将笼统地讨论 DI。这意味着我们不会使用任何所谓的DI Container。完全可以在不使用DI Container的情况下应用 DI 。DI 容器是一个有用但可选的工具。因此第 1、2 和 3 部分或多或少完全忽略了DI 容器,而是以与容器无关的方式讨论 DI。然后,在第 4 部分中,我们返回到DI 容器来剖析三个特定的库。

In this part, we’ll discuss DI in general terms. This means we won’t use any so-called DI Container. It’s entirely possible to apply DI without using a DI Container. A DI Container is a helpful, but optional, tool. So parts 1, 2, and 3 more or less ignore DI Containers completely, and instead discuss DI in a container-agnostic way. Then, in part 4, we return to DI Containers to dissect three specific libraries.

第 1 部分为本书的其余部分建立了上下文。它面向没有任何 DI 知识的读者,但经验丰富的 DI 从业者也可以通过浏览各章来了解整本书中使用的术语,从而从中受益。到第 1 部分结束时,您应该牢牢掌握词汇和整体概念,即使某些具体细节仍然有些模糊。没关系——随着您的阅读,本书变得更加具体,因此第 2、3 和 4 部分应该回答您在阅读第 1 部分后可能会遇到的问题。

Part 1 establishes the context for the rest of the book. It’s aimed at readers who don’t have any prior knowledge of DI, but experienced DI practitioners can also benefit from skimming the chapters to get a feeling for the terminology used throughout the book. By the end of part 1, you should have a firm grasp of the vocabulary and overall concepts, even if some of the concrete details are still a little fuzzy. That’s OK — the book becomes more concrete as you read on, so parts 2, 3, and 4 should answer the questions you’re likely to have after reading part 1.

1

依赖注入的基础知识:什么、为什么以及如何

1

The basics of Dependency Injection: What, why, and how

在这一章当中

In this chapter

  • 消除关于依赖注入的常见误解
  • Dispelling common myths about Dependency Injection
  • 理解依赖注入的目的
  • Understanding the purpose of Dependency Injection
  • 评估依赖注入的好处
  • Evaluating the benefits of Dependency Injection
  • 知道何时应用依赖注入
  • Knowing when to apply Dependency Injection

您可能听说过制作贝恩酱酱很困难。即使在经常做饭的人中,也有许多人从未尝试过做饭。真可惜,因为酱汁很好吃。(它传统上与牛排搭配,但它也是白芦笋、荷包蛋和其他菜肴的绝佳搭配。)一些人求助于现成的酱汁或速溶混合物等替代品,但这些并不像真正的那样令人满意.

You may have heard that making a sauce béarnaise is difficult. Even among people who regularly cook, many have never attempted to make one. This is a shame, because the sauce is delicious. (It’s traditionally paired with steak, but it’s also an excellent accompaniment to white asparagus, poached eggs, and other dishes.) Some resort to substitutes like ready-made sauces or instant mixes, but these aren’t nearly as satisfying as the real thing.

蛋黄酱酱是一种由蛋黄和黄油制成的乳化酱汁,用龙蒿、山萝卜、青葱和醋调味。它不含水。制作它的最大挑战是它的准备工作可能会失败。酱汁会凝结或分离,如果发生任何一种情况,您将无法复活它。准备时间大约需要 45 分钟,因此尝试失败意味着您可能没有时间进行第二次尝试。在另一方面,任何厨师都可以制作蛋黄酱。这是他们训练的一部分,正如他们会告诉你的那样,这并不困难。

A sauce béarnaise is an emulsified sauce made from egg yolk and butter, that’s flavored with tarragon, chervil, shallots, and vinegar. It contains no water. The biggest challenge to making it is that its preparation can fail. The sauce can curdle or separate, and, if either happens, you can’t resurrect it. It takes about 45 minutes to prepare, so a failed attempt means that you may not have time for a second try. On the other hand, any chef can prepare a sauce béarnaise. It’s part of their training and, as they’ll tell you, it’s not difficult.

您无需成为专业厨师即可制作蛋黄酱。任何学习制作它的人都至少会失败一次,但是在掌握了它之后,您每次都会成功。我们认为依赖注入(DI)就像蛋黄酱。它被认为是困难的,而且,如果您尝试使用它但失败了,很可能没有时间进行第二次尝试。

You don’t have to be a professional cook to make sauce béarnaise. Anyone learning to make it will fail at least once, but after you get the hang of it, you’ll succeed every time. We think Dependency Injection (DI) is like sauce béarnaise. It’s assumed to be difficult, and, if you try to use it and fail, it’s likely there won’t be time for a second attempt.

尽管有恐惧、不确定和怀疑(恐惧症) 围绕 DI,就像制作贝恩酱一样简单易学。学习时可能会犯错误,但一旦掌握了技巧,就再也不会失败。

Despite the fear, uncertainty, and doubt (FUD) surrounding DI, it’s as easy to learn as making a sauce béarnaise. You may make mistakes while you learn, but once you’ve mastered the technique, you’ll never again fail to apply it successfully.

软件开发问答网站 Stack Overflow 对“如何向 5 岁的孩子解释依赖注入?”这个问题提供了答案。John Munsch 给出的评分最高的答案提供了一个针对(想象中的)五岁调查官的非常准确的类比:1 

Stack Overflow, the software development Q&A website, features an answer to the question, “How to explain Dependency Injection to a 5-year old?” The most highly rated answer, by John Munsch, provides a surprisingly accurate analogy targeted at the (imaginary) five-year-old inquisitor:1 

当您自己从冰箱中取出东西时,可能会引起问题。你可能会把门开着,你可能会得到妈妈或爸爸不希望你拥有的东西。您甚至可能正在寻找我们甚至没有或已经过期的东西。

When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.

你应该做的是陈述需求,“我需要在午餐时喝点东西,”然后我们会确保你坐下来吃饭时有东西喝。

What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.

这在面向对象的软件开发方面意味着:协作类(五岁)应该依赖基础设施(父母)来提供必要的服务。

What this means in terms of object-oriented software development is this: collaborating classes (the five-year-old) should rely on infrastructure (the parents) to provide necessary services.

本章在结构上相当线性。首先,我们介绍 DI,包括它的目的和好处。尽管我们包含示例,但总的来说,本章的代码少于本书中的任何其他章节。在介绍 DI 之前,我们先讨论 DI 的基本目的——可维护性。这很重要,因为如果您没有做好充分准备,很容易误解 DI。接下来,在一个例子(你好 DI!)之后,我们讨论了好处和范围,为本书制定了路线图。完成本章后,您应该为本书其余部分中更高级的概念做好准备。

This chapter is fairly linear in structure. First, we introduce DI, including its purpose and benefits. Although we include examples, overall, this chapter has less code than any other chapter in the book. Before we introduce DI, we discuss the basic purpose of DI — maintainability. This is important because it’s easy to misunderstand DI if you aren’t properly prepared. Next, after an example (Hello DI!), we discuss benefits and scope, laying out a road map for the book. When you’re done with this chapter, you should be prepared for the more advanced concepts in the rest of the book.

对于大多数开发人员而言,DI 似乎是一种相当落后的创建源代码的方法,而且,就像蛋黄酱一样,其中涉及很多 FUD。要了解 DI,首先要了解它的用途。

To most developers, DI may seem like a rather backward way of creating source code, and, like sauce béarnaise, there’s much FUD involved. To learn about DI, you must first understand its purpose.

1.1 编写可维护的代码

1.1 Writing maintainable code

DI 的作用是什么?DI 本身不是目标;相反,它是达到目的的一种手段。归根结底,大多数编程技术的目的是尽可能高效地交付可工作的软件。其中一方面是编写可维护的代码。

What purpose does DI serve? DI isn’t a goal in itself; rather, it’s a means to an end. Ultimately, the purpose of most programming techniques is to deliver working software as efficiently as possible. One aspect of that is to write maintainable code.

除非您只编写原型或从未通过其第一个版本的应用程序,否则您会发现自己在维护和扩展现有代码库。为了有效地使用此类代码库,一般来说,它们的可维护性越高越好。

Unless you only write prototypes, or applications that never make it past their first release, you find yourself maintaining and extending existing code bases. To work effectively with such code bases, in general, the more maintainable they are, the better.

使代码更易于维护的一个极好的方法是通过松散耦合。早在 1994 年,当四人组编写设计模式时,这已经是常识:2 

An excellent way to make code more maintainable is through loose coupling. As far back as 1994, when the Gang of Four wrote Design Patterns, this was already common knowledge:2 

针对接口而不是实现编程。

Program to an interface, not an implementation.

这条重要的建议不是结论,而是设计模式的前提。松耦合使代码可扩展,可扩展性使其可维护。DI 只不过是一种支持松散耦合的技术。此外,关于 DI 存在许多误解,有时会妨碍正确理解。在你可以学习之前,你必须忘记你已经知道的(你认为的)东西。

This important piece of advice isn’t the conclusion, but, rather, the premise of Design Patterns. Loose coupling makes code extensible, and extensibility makes it maintainable. DI is nothing more than a technique that enables loose coupling. Moreover, there are many misconceptions about DI, and sometimes they get in the way of proper understanding. Before you can learn, you must unlearn what (you think) you already know.

1.1.1 关于 DI 的常见误解

1.1.1 Common myths about DI

您以前可能从未遇到或听说过 DI,这很好。跳过本节,直接进入 1.1.2 节。但是,如果您正在阅读本书,您可能至少在谈话中、在您继承的代码库中或在博客文章中遇到过它。您可能还注意到它带有相当多的严厉意见。在本节中,我们将探讨多年来出现的关于 DI 的四种最常见的误解,以及它们为何不正确。这些神话包括以下内容:

You may never have come across or heard of DI before, and that’s great. Skip this section and go straight to section 1.1.2. But, if you’re reading this book, it’s likely you’ve at least come across it in conversation, in a code base you inherited, or in blog posts. You may also have noticed that it comes with a fair amount of heavy opinions. In this section, we’re going to look at four of the most common misconceptions about DI that have appeared over the years and why they aren’t true. These myths include the following:

  • DI 仅与后期绑定相关。
  • DI is only relevant for late binding.
  • DI 仅与单元测试相关。
  • DI is only relevant for unit testing.
  • DI 是一种类固醇抽象工厂
  • DI is a sort of Abstract Factory on steroids.
  • DI 需要一个DI Container
  • DI requires a DI Container.

尽管这些神话都不是真的,但它们仍然很流行。在您开始了解 DI 之前,我们需要消除它们。

Although none of these myths are true, they’re prevalent nonetheless. We need to dispel them before you can start to learn about DI.

后期绑定

Late binding

在此上下文中,后期绑定是指无需重新编译代码即可替换应用程序部分的能力。启用第三方加载项的应用程序(例如 Visual Studio) 就是一个例子。另一个例子是支持不同运行环境的标准软件。

In this context, late binding refers to the ability to replace parts of an application without recompiling the code. An application that enables third-party add-ins (such as Visual Studio) is one example. Another example is the standard software that supports different runtime environments.

假设您有一个运行在多个数据库引擎上的应用程序(例如,同时支持 Oracle 和 SQL Server 的)。为了支持这个功能,应用程序的其余部分通过接口与数据库对话. 代码库提供了此接口的不同实现,以分别访问 Oracle 和 SQL Server。在这种情况下,您可以使用配置选项来控制给定安装应使用哪个实现。

Suppose you have an application that runs on more than one database engine (for example, one that supports both Oracle and SQL Server). To support this feature, the rest of the application talks to the database through an interface. The code base provides different implementations of this interface to access Oracle and SQL Server, respectively. In this case, you can use a configuration option to control which implementation should be used for a given installation.

一种常见的误解是 DI 仅与此类场景相关。这是可以理解的,因为 DI 支持这种情况。但谬论是认为这种关系是对称的。DI 启用后期绑定这一事实并不意味着它只与后期绑定场景相关。如图1.1所示,后期绑定只是 DI 的众多方面之一。

It’s a common misconception that DI is only relevant for this sort of scenario. That’s understandable, because DI enables this scenario. But the fallacy is to think that the relationship is symmetric. The fact that DI enables late binding doesn’t mean that it’s only relevant in late-binding scenarios. As figure 1.1 illustrates, late binding is only one of the many aspects of DI.

01-01.tif

图 1.1 延迟绑定由 DI 启用,但假设它仅适用于延迟绑定场景是对更广阔前景的狭隘看法。

Figure 1.1 Late binding is enabled by DI, but to assume that it’s only applicable in late-binding scenarios is to adopt a narrow view of a much broader vista.

如果您认为 DI 仅与后期绑定场景相关,那么您需要忘却这一点。DI 的作用远不止启用后期绑定。

If you thought that DI was only relevant for late-binding scenarios, this is something you need to unlearn. DI does much more than enable late binding.

单元测试

Unit testing

有些人认为 DI 只与支持单元测试有关。这也不是真的,尽管 DI 肯定是支持单元测试的重要部分。说实话,我们最初对 DI 的介绍来自于与测试驱动开发的某些方面的斗争(测试驱动开发). 在那段时间里,我们发现了 DI 并了解到其他人已经使用它来支持我们正在处理的一些相同场景。

Some people think that DI is only relevant for supporting unit testing. This isn’t true, either, although DI is certainly an important part of support for unit testing. To tell you the truth, our original introduction to DI came from struggling with certain aspects of Test-Driven Development (TDD). During that time, we discovered DI and learned that other people had used it to support some of the same scenarios we were addressing.

即使您不编写单元测试(如果您不这样做,您应该现在就开始),DI 仍然很重要,因为它提供了所有其他好处。声称 DI 仅与支持单元测试相关就像声称它仅与支持后期绑定相关一样。图 1.2显示虽然这是一个不同的视图,但它是一个与图 1.1一样狭窄的视图。在本书中,我们将尽力向您展示全貌。

Even if you don’t write unit tests (if you don’t, you should start now), DI is still relevant because of all the other benefits it offers. Claiming that DI is only relevant for supporting unit testing is like claiming that it’s only relevant for supporting late binding. Figure 1.2 shows that although this is a different view, it’s a view as narrow as figure 1.1. In this book, we’ll do our best to show you the whole picture.

01-02.tif

图 1.2 也许您一直假设单元测试是 DI 的唯一目的。尽管该假设与后期绑定假设的观点不同,但它也是更广阔前景的狭隘观点。

Figure 1.2 Perhaps you’ve been assuming that unit testing is the sole purpose of DI. Although that assumption is a different view than the late-binding assumption, it, too, is a narrow view of a much broader vista.

如果您认为 DI 仅与单元测试相关,请忘掉这个假设。DI 的作用远不止启用单元测试。

If you thought that DI was only relevant for unit testing, unlearn this assumption. DI does much more than enable unit testing.

类固醇的抽象工厂

An Abstract Factory on steroids

也许最危险的谬误是 DI 涉及某种通用的抽象工厂,您可以使用它来创建应用程序所需的依赖项的实例。

Perhaps the most dangerous fallacy is that DI involves some sort of general-purpose Abstract Factory that you can use to create instances of the Dependencies needed in your applications.

在本章的介绍中,我们写道“协作类应该依赖基础设施来提供必要的服务”。你对这句话的最初想法是什么?您是否将基础设施视为某种服务,您可以通过查询来获取所需的依赖项?如果是这样,你并不孤单。许多开发人员和架构师将 DI 视为可用于定位其他服务的服务。这称为服务定位器,但它与 DI 正好相反。

In the introduction to this chapter, we wrote that “collaborating classes should rely on infrastructure to provide necessary services.” What were your initial thoughts about this sentence? Did you think about infrastructure as some sort of service you could query to get the Dependencies you need? If so, you aren’t alone. Many developers and architects think about DI as a service that can be used to locate other services. This is called a Service Locator, but it’s the exact opposite of DI.

服务定位器通常被称为类固醇的抽象工厂,因为与普通的抽象工厂相比,可解析类型的列表是未指定的并且可能是无穷无尽的。它通常有一个方法允许创建各种类型,如下所示:

A Service Locator is often called an Abstract Factory on steroids because, compared to a normal Abstract Factory, the list of resolvable types is unspecified and possibly endless. It typically has one method allowing the creation of all sorts of types, much like in the following:

public interface IServiceLocator
{
    object GetService(Type serviceType);
}

DI容器

DI Containers

与先前的误解密切相关的是 DI 需要DI Container的概念。如果您之前错误地认为 DI 涉及Service Locator,那么很容易得出结论,DI Container可以承担Service Locator的责任。可能是这种情况,但这根本不是您应该如何使用DI Container

Closely associated with the previous misconception is the notion that DI requires a DI Container. If you held the previous, mistaken belief that DI involves a Service Locator, then it’s easy to conclude that a DI Container can take on the responsibility of the Service Locator. This might be the case, but it’s not at all how you should use a DI Container.

DI 容器是一个可选的库,它可以让您在连接应用程序时更轻松地组合类,但这绝不是必需的。当您在没有DI Container的情况下编写应用程序时,它被称为Pure DI。这可能需要更多的工作,但除此之外,您不必在任何 DI 原则上妥协。

A DI Container is an optional library that makes it easier to compose classes when you wire up an application, but it’s in no way required. When you compose applications without a DI Container, it’s called Pure DI. It might take a little more work, but other than that, you don’t have to compromise on any DI principles.

我们还没有准确解释DI 容器是什么,以及您应该如何以及何时使用它。我们将在第 3 章末尾对此进行更详细的介绍;第 4 部分完全致力于此。

We have yet to explain exactly what a DI Container is, and how and when you should use it. We’ll go into more detail on this at the end of chapter 3; part 4 is completely dedicated to it.

您可能会认为,虽然我们已经揭露了关于 DI 的四个神话,但我们还没有对其中任何一个提出令人信服的理由。这是真的。从某种意义上说,这本书是对这些常见误解的有力论证,因此我们稍后肯定会回到这些主题。例如,在第 5 章中,5.2 节讨论了为什么使用Service Locator是一种反模式。

You may think that, although we’ve exposed four myths about DI, we have yet to make a compelling case against any of them. That’s true. In a sense, this book is one big argument against these common misconceptions, so we’ll certainly return to these topics later. For example, in chapter 5, section 5.2 discusses why Service Locator is an anti-pattern.

根据我们的经验,忘掉学习是至关重要的,因为人们经常试图改造我们告诉他们的关于 DI 的内容,并将其与他们认为自己已经知道的内容保持一致。当这种情况发生时,他们需要一段时间才能最终意识到他们的一些最基本的假设是错误的。我们不想让你有这种经历。如果可以,请像对 DI 一无所知一样阅读本书。

In our experience, unlearning is vital because people often try to retrofit what we tell them about DI and align it with what they think they already know. When this happens, it takes time before it finally dawns on them that some of their most basic assumptions are wrong. We want to spare you that experience. If you can, read this book as though you know nothing about DI.

1.1.2 理解 DI 的目的

1.1.2 Understanding the purpose of DI

DI 不是最终目标——它是达到目的的手段。DI 实现了松散耦合,松散耦合使代码更易于维护。这是一个相当大的说法,虽然我们可以将您推荐给像四人帮这样的权威机构以了解详细信息,但我们发现解释为什么这是真的是公平的。

DI isn’t an end goal — it’s a means to an end. DI enables loose coupling, and loose coupling makes code more maintainable. That’s quite a claim, and although we could refer you to well-established authorities like the Gang of Four for details, we find it only fair to explain why this is true.

为了传达这一信息,下一节将软件设计和几种软件设计模式与电线进行比较。我们发现这是一个强有力的类比。我们甚至用它来向非技术人员解释软件设计。

To get this message across, the next section compares software design and several software design patterns with electrical wiring. We’ve found this to be a powerful analogy. We even use it to explain software design to non-technical people.

我们在这个类比中使用了四种特定的设计模式,因为它们经常与 DI 相关。在整本书中,您会看到其中三种模式的许多示例——装饰器、合成器和适配器。(我们将在第 4 章介绍第四种模式,即 Null Object 模式。)如果您对这些模式还不是很熟悉,请不要担心:您将在本书的最后熟悉这些模式。

We use four specific design patterns in this analogy because they occur frequently in relation to DI. You’ll see many examples of three of these patterns — Decorator, Composite, and Adapter — throughout this book. (We cover the fourth, the Null Object pattern, in chapter 4.) Don’t worry if you’re not that familiar with these patterns: you will be by the end of the book.

软件开发仍然是一个相当新的职业,所以在很多方面我们仍在弄清楚如何实现良好的架构。但是,在更传统的行业(如建筑)中具有专业知识的人很久以前就想通了。

Software development is still a rather new profession, so in many ways we’re still figuring out how to implement good architecture. But individuals with expertise in more traditional professions (such as construction) figured it out a long time ago.

入住廉价酒店

Checking into a cheap hotel

如果你住在便宜的旅馆,你可能会遇到如图 1.3 所示的景象。在这里,为了您的方便,酒店好心地提供了吹风机,但显然他们不信任您将吹风机留给下一位客人:该设备直接连接到墙上的插座。酒店管理层认为,更换被盗吹风机的成本高到足以证明在其他情况下实施明显较差的做法是合理的。

If you’re staying at a cheap hotel, you might encounter a sight like the one in figure 1.3. Here, the hotel has kindly provided a hair dryer for your convenience, but apparently they don’t trust you to leave the hair dryer for the next guest: the appliance is directly attached to the wall outlet. The hotel management decided that the cost of replacing stolen hair dryers is high enough to justify what’s otherwise an obviously inferior implementation.

01-03.tif

图 1.3 在便宜的旅馆房间里,您可能会发现吹风机直接连接到墙上的插座。这相当于使用编写紧耦合代码的常见做法。

Figure 1.3 In a cheap hotel room, you might find a hair dryer wired directly into the wall outlet. This is equivalent to using the common practice of writing tightly coupled code.

当吹风机停止工作时会发生什么?酒店必须请一位技术娴熟的专业人员。要修理固定接线的吹风机,必须切断房间的电源,使其暂时无法使用。然后,技术人员必须使用特殊工具断开吹风机的连接,并更换新的吹风机。如果你幸运的话,技术人员会记得重新打开房间的电源,然后回去测试新吹风机是否工作——如果你幸运的话。这个过程听起来很熟悉吗?

What happens when the hair dryer stops working? The hotel has to call in a skilled professional. To fix the hardwired hair dryer, the power to the room will have to be cut, rendering it temporarily useless. Then, the technician must use special tools to disconnect the hair dryer and replace it with a new one. If you’re lucky, the technician will remember to turn the power to the room back on and go back to test whether the new hair dryer works — if you’re lucky. Does this procedure sound at all familiar?

这就是您处理紧密耦合代码的方法。在这种情况下,吹风机与墙壁紧密相连,您无法在不影响另一个的情况下轻松修改其中一个。

This is how you would approach working with tightly coupled code. In this scenario, the hair dryer is tightly coupled to the wall, and you can’t easily modify one without impacting the other.

将电线与设计模式进行比较

Comparing electrical wiring to design patterns

通常,我们不会通过将电缆直接连接到墙上来将电器连接在一起。相反,如图 1.4 所示,我们使用插头和插座。插座定义了插头必须匹配的形状。

Usually, we don’t wire electrical appliances together by attaching the cable directly to the wall. Instead, as in figure 1.4, we use plugs and sockets. A socket defines a shape that the plug must match.

类比于软件设计,插座是一个接口,而插头及其设备是一个实现。这意味着房间(应用程序)有一个或(希望)多个插座,房间的用户(开发人员)可以随心所欲地插入电器,甚至可能是客户提供的吹风机。

In an analogy to software design, the socket is an interface, and the plug with its appliance is an implementation. This means that the room (the application) has one or (hopefully) more sockets, and the users of the room (the developers) can plug in appliances as they please, potentially even a customer-supplied hair dryer.

01-04.eps

图 1.4 通过使用插座和插头,吹风机可以松散地连接到墙上的插座。

Figure 1.4 Through the use of sockets and plugs, a hair dryer can be loosely coupled to a wall outlet.

与硬连线吹风机相比,插头和插座定义了一种用于连接电器的松散耦合模型。只要插头(实现)适合插座(实现接口),并且它可以处理伏特和赫兹的量(遵守接口契约),我们就可以以多种方式组合电器。特别有趣的是,许多这些常见的组合可以与众所周知的软件设计原则和模式进行比较。

In contrast to the hardwired hair dryer, plugs and sockets define a loosely coupled model for connecting electrical appliances. As long as the plug (the implementation) fits into the socket (implements the interface), and it can handle the amount of volts and hertz (obeys the interface contract), we can combine appliances in a variety of ways. What’s particularly interesting is that many of these common combinations can be compared to well-known software design principles and patterns.

首先,我们不再局限于吹风机。如果您是普通读者,我们会猜测您对计算机的需求远远超过对吹风机的需求。这不是问题:你拔掉吹风机的插头,然后把电脑的插头插到同一个插座上(图 1.5)。

First, we’re no longer constrained to hair dryers. If you’re an average reader, we would guess that you need power for a computer much more than you do for a hair dryer. That’s not a problem: you unplug the hair dryer and plug a computer into the same socket (figure 1.5).

01-05.eps

图 1.5使用一个插座和一个插头,您可以用电脑替换图 1.4中 的原始吹风机。这对应于Liskov 替换原则

Figure 1.5 Using a socket and a plug, you can replace the original hair dryer from figure 1.4 with a computer. This corresponds to the Liskov Substitution Principle.

如果您暂时不需要使用计算机,您可以拔掉它。即使没有插入任何东西,房间也不会爆炸。也就是说,如果你把电脑的插头从墙上拔下来,墙上的插座和电脑都不会坏。

You can unplug the computer if you don’t need to use it at the moment. Even though nothing is plugged in, the room doesn’t explode. That is to say, if you unplug the computer from the wall, neither the wall outlet nor the computer breaks down.

但是,对于软件,客户通常希望获得可用的服务。如果您删除该服务,您将获得一个. 要处理这种情况,您可以创建一个不执行任何操作的接口实现。这种称为Null Object的设计模式对应于儿童安全插座插头(没有电线或电器但仍可插入插座的插头)。而且因为您使用的是松散耦合,所以您可以用不做任何事情的东西替换真正的实现而不会引起麻烦。如图 1.6所示。NullReferenceException

With software, however, a client often expects a service to be available. If you remove the service, you get a NullReferenceException. To deal with this type of situation, you can create an implementation of an interface that does nothing. This design pattern, known as Null Object, corresponds to having a children’s safety outlet plug (a plug without a wire or appliance that still fits into the socket). And because you’re using loose coupling, you can replace a real implementation with something that does nothing without causing trouble. This is illustrated in figure 1.6.

01-06.eps

图 1.6 用儿童安全插座插头替换时,拔下计算机插头不会导致房间和计算机爆炸。这可以粗略地比作空对象模式。

Figure 1.6 Unplugging the computer causes neither room nor computer to explode when replaced with a children’s safety outlet plug. This can be roughly likened to the Null Object pattern.

您还可以做许多其他事情。如果您住在间歇性停电的社区,您可能希望通过插入不间断电源来保持计算机运行(UPS). 如图 1.7所示,您将 UPS 连接到墙上插座,将计算机连接到 UPS。

There are many other things you can do, as well. If you live in a neighborhood with intermittent power failures, you may want to keep the computer running by plugging in into an uninterrupted power supply (UPS). As shown in figure 1.7, you connect the UPS to the wall outlet and the computer to the UPS.

01-07.eps

图 1.7 可引入 UPS 以在停电时保持计算机运行。这对应于装饰器设计模式。

Figure 1.7 A UPS can be introduced to keep the computer running in case of power failure. This corresponds to the Decorator design pattern.

计算机和 UPS 有不同的用途。每个人都有一个不侵犯另一个单位的单一责任。UPS和电脑很可能是两个不同厂家生产的,不同时间买的,分开插的。如图1.5所示,您可以在没有 UPS 的情况下运行计算机,并且您也可以想象在停电期间通过将其插入 UPS 来使用吹风机。

The computer and the UPS serve separate purposes. Each has a Single Responsibility that doesn’t infringe on the other unit. The UPS and computer are likely to be produced by two different manufacturers, bought at different times, and plugged in separately. As figure 1.5 demonstrated, you can run the computer without a UPS, and you could also conceivably use the hair dryer during blackouts by plugging it into the UPS.

在软件设计中,这种用同一接口的另一个实现拦截一个实现的方式被称为装饰器设计模式。5  它使您能够逐步引入新功能和横切关注点无需重写或更改大量现有代码。

In software design, this way of intercepting one implementation with another implementation of the same interface is known as the Decorator design pattern.5  It gives you the ability to incrementally introduce new features and Cross-Cutting Concerns without having to rewrite or change a lot of existing code.

01-08.eps

图 1.8 电源板可以将多个电器插入一个墙上插座。这对应于复合设计模式。

Figure 1.8 A power strip makes it possible to plug several appliances into a single wall outlet. This corresponds to the Composite design pattern.

向现有代码库添加新功能的另一种方法是使用新实现重构接口的现有实现。当您将多个实现聚合为一个时,您使用复合设计模式. 6  图 1.8说明了这与将不同的电器插入电源板是如何对应的。

Another way to add new functionality to an existing code base is to refactor an existing implementation of an interface with a new implementation. When you aggregate several implementations into one, you use the Composite design pattern.6  Figure 1.8 illustrates how this corresponds to plugging diverse appliances into a power strip.

配电盘只有一个插头,您可以将其插入一个插座,配电盘本身为各种电器提供多个插座。这使您可以在计算机运行时添加和移除吹风机。以同样的方式,复合模式使得通过修改组合接口实现集来添加或删除功能变得容易。

The power strip has a single plug that you can insert into a single socket, and the power strip itself provides several sockets for a variety of appliances. This enables you to add and remove the hair dryer while the computer is running. In the same way, the Composite pattern makes it easy to add or remove functionality by modifying the set of composed interface implementations.

这是最后一个例子。有时您会发现自己处于插头不适合特定插座的情况。如果您去过另一个国家/地区,您可能会注意到世界各地的插座各不相同。如果你在旅行时随身携带类似图 1.9中的相机的东西,你将需要一个适配器来给它充电。适当地,有一个同名的设计模式。

Here’s a final example. You sometimes find yourself in situations where a plug doesn’t fit into a particular socket. If you’ve traveled to another country, you’ve likely noticed that sockets differ across the world. If you bring something like the camera in figure 1.9 along when traveling, you’ll need an adapter to charge it. Appropriately, there’s a design pattern with the same name.

01-09.eps

图 1.9 旅行时,经常需要使用适配器将电器插入外国插座(例如给相机充电)。这对应于Adapter 设计模式。有时,转换就像改变插头的形状一样简单,或者像将电流从交流电 (AC) 变为直流电 (DC) 一样复杂。

Figure 1.9 When traveling, you often need to use an adapter to plug an appliance into a foreign socket (for example, to recharge a camera). This corresponds to the Adapter design pattern. Sometimes, translation is as simple as changing the shape of the plug, or as complex as changing the electric current from alternating current (AC) to direct current (DC).

适配器设计模式就像它的物理同名一样工作。7  您可以使用它来匹配两个相关但独立的接口。当您有一个现有的第三方 API,您希望将其公开为您的应用程序使用的接口的实例时,这尤其有用。与物理适配器一样,适配器设计模式的实现可以从简单到极其复杂。

The Adapter design pattern works like its physical namesake.7  You can use it to match two related, yet separate, interfaces to each other. This is particularly useful when you have an existing third-party API that you want to expose as an instance of an interface your application consumes. As with the physical adapter, implementations of the Adapter design pattern can range from simple to extremely complex.

插座和插头模型的惊人之处在于,几十年来,它被证明是一种简单且用途广泛的模型。一旦基础设施到位,它就可以被使用任何人并适应不断变化的需求和意外要求。更有趣的是,当我们将此模型与软件开发相关联时,所有构建块都已经以设计原则和模式的形式就位。

What’s amazing about the socket and plug model is that, over decades, it’s proven to be an easy and versatile model. Once the infrastructure is in place, it can be used by anyone and adapted to changing needs and unanticipated requirements. What’s even more interesting is that, when we relate this model to software development, all the building blocks are already in place in the form of design principles and patterns.

松散耦合在软件设计中的优势与在物理插座和插头模型中的优势相同:一旦基础设施到位,任何人都可以使用它并适应不断变化的需求和不可预见的要求,而无需对应用程序进行大的更改代码库和基础设施。这意味着理想情况下,新需求应该只需要添加一个新类,而无需更改系统中其他已经存在的类。

The advantage of loose coupling is the same in software design as it is in the physical socket and plug model: Once the infrastructure is in place, it can be used by anyone and adapted to changing needs and unforeseen requirements without requiring large changes to the application code base and infrastructure. This means that ideally, a new requirement should only necessitate the addition of a new class, with no changes to other already-existing classes of the system.

这种能够在不修改现有代码的情况下扩展应用程序的概念称为开放/封闭原则. 不可能达到 100% 的代码始终对可扩展性开放而对修改关闭的情况。不过,松散耦合确实会让您更接近该目标。

This concept of being able to extend an application without modifying existing code is called the Open/Closed Principle. It’s impossible to get to a situation where 100% of your code will always be open for extensibility and closed for modification. Still, loose coupling does bring you closer to that goal.

而且,随着每一步,向您的系统添加新功能和要求变得更加容易。能够在不触及系统现有部分的情况下添加新功能意味着问题是孤立的。这导致代码更易于理解和测试,使您能够管理系统的复杂性。这就是松散耦合可以为您提供帮助的原因,也是它可以使代码库更易于维护的原因。我们将在第 4 章中更详细地讨论开闭原则

And, with every step, it gets easier to add new features and requirements to your system. Being able to add new features without touching existing parts of the system means that problems are isolated. This leads to code that’s easier to understand and test, allowing you to manage the complexity of your system. That’s what loose coupling can help you with, and that’s why it can make a code base much more maintainable. We’ll discuss the Open/Closed Principle in more detail in chapter 4.

到目前为止,您可能想知道这些模式在代码中实现时的外观。别担心。如前所述,我们将在整本书中向您展示这些模式的大量示例。事实上,在本章的后面,我们将向您展示装饰器模式和适配器模式的实现。

By now you might be wondering how these patterns will look when implemented in code. Don’t worry about that. As we stated before, we’ll show you plenty of examples of those patterns throughout this book. In fact, later in this chapter, we’ll show you an implementation of both the Decorator and Adapter patterns.

松散耦合的简单部分是针对接口而不是实现进行编程。问题是,“实例来自哪里?” 从某种意义上说,这就是整本书的主题:这是 DI 试图回答的核心问题。

The easy part of loose coupling is programming to an interface instead of an implementation. The question is, “Where do the instances come from?” In a sense, this is what this entire book is about: it’s the core question that DI seeks to answer.

您不能像创建具体类型的新实例那样创建接口的新实例。像这样的代码无法编译:

You can’t create a new instance of an interface the same way that you create a new instance of a concrete type. Code like this doesn’t compile:

01-14_hedgehog.eps

接口不包含任何实现,因此这是不可能的。writer实例_必须使用不同的机制创建。DI 解决了这个问题。有了这个 DI 目的的概述,我们认为您已经准备好举个例子了。

An interface contains no implementation, so this isn’t possible. The writer instance must be created using a different mechanism. DI solves this problem. With this outline of the purpose of DI, we think you’re ready for an example.

1.2 一个简单的例子:你好DI!

1.2 A simple example: Hello DI!

在无数编程教科书的传统中,让我们来看看一个简单的控制台应用程序,上面写着“Hello DI!” 到屏幕。请注意,完整代码可作为本书下载的一部分提供,如本书开头的“代码约定和下载”部分所述。

In the tradition of innumerable programming textbooks, let’s take a look at a simple console application that writes “Hello DI!” to the screen. Note that the full code is available as part of the download for this book, as mentioned in the section “Code conventions and downloads” at the beginning of this book.

在本节中,我们将向您展示代码的外观,并简要概述一些主要优点,但不深入细节。在本书的其余部分,我们将更加具体。

In this section, we’ll show you what the code looks like and briefly outline some key benefits without going into details. In the rest of the book, we’ll get more specific.

1.2.1 你好迪!代码

1.2.1 Hello DI! code

您可能习惯于看到用一行代码编写的 Hello World 示例。在这里,我们将把一些非常简单的东西变得更复杂。为什么?我们很快就会谈到这一点,但让我们先看看 Hello World 使用 DI 会是什么样子。

You’re probably used to seeing Hello World examples that are written with a single line of code. Here, we’ll take something that’s extremely simple and make it more complicated. Why? We’ll get to that shortly, but let’s first see what Hello World would look like with DI.

合作者

Collaborators

为了了解程序的结构,我们将从查看Main方法开始控制台应用程序。然后我们将向您展示协作类;但首先,这是MainHello DI 的方法!应用:

To get a sense of the structure of the program, we’ll start by looking at the Main method of the console application. Then we’ll show you the collaborating classes; but first, here’s the Main method of the Hello DI! application:

private static void Main()
{
    IMessageWriter writer = new ConsoleMessageWriter();
    var salutation = new Salutation(writer);
    salutation.Exclaim();
}

因为程序需要写入控制台,所以它创建了一个封装该功能的新实例。它将那个消息编写器传递给类ConsoleMessageWriterSalutation这样称呼实例就知道在哪里写它的消息。因为现在一切都已正确连接,您可以通过Exclaim方法执行逻辑,从而将消息写入屏幕。

Because the program needs to write to the console, it creates a new instance of ConsoleMessageWriter that encapsulates that functionality. It passes that message writer to the Salutation class so that the salutation instance knows where to write its messages. Because everything is now wired up properly, you can execute the logic via the Exclaim method, which results in the message being written to the screen.

Main方法内部对象的构造纯 DI的基本示例. 没有DI 容器用于组成Salutation及其依赖项图 1.10显示了协作者之间的关系。ConsoleMessageWriter

The construction of objects inside the Main method is a basic example of Pure DI. No DI Container is used to compose the Salutation and its ConsoleMessageWriter Dependency. Figure 1.10 shows the relationship between the collaborators.

01-10.eps

图 1.10 Hello DI! 合作者之间的关系 应用

Figure 1.10 Relationship between the collaborators of the Hello DI! application

实现应用程序逻辑

Implementing the application logic

应用的主要逻辑封装在Salutation类中,如清单 1.1所示。

The main logic of the application is encapsulated in the Salutation class, shown in listing 1.1.

Listing 1.1 Salutation类封装了主要的应用逻辑

Listing 1.1 Salutation class encapsulates the main application logic

public class Salutation
{
    private readonly IMessageWriter writer;

    public Salutation(IMessageWriter writer)    ①  
    {
        if (writer == null)    ②  
            throw new ArgumentNullException("writer");  ②  

        this.writer = writer;
    }

    public void Exclaim()
    {
        this.writer.Write("Hello DI!");    ③  
    }
}

Salutation班级_取决于一个名为(接下来定义)的自定义接口。它通过其构造函数请求它的一个实例。这种做法称为构造函数注入IMessageWriter. 保护条款IMessageWriter通过抛出异常 来验证提供的不为空。8  最后,您在方法IMessageWriter的实现中使用之前注入的实例Exclaim通过调用它的Write方法. 这会发送 Hello DI!消息到IMessageWriter Dependency

The Salutation class depends on a custom interface called IMessageWriter (defined next). It requests an instance of it through its constructor. This practice is called Constructor Injection. A Guard Clause verifies that the supplied IMessageWriter isn’t null by throwing an exception if it is.8  And, finally, you use the previously injected IMessageWriter instance inside the implementation of the Exclaim method by calling its Write method. This sends the Hello DI! message to the IMessageWriter Dependency.

用 DI 术语来说,我们说IMessageWriter Dependency被注入到Salutation类中使用构造函数参数。请注意,Salutation没有意识。它仅通过界面与之交互。是为该场合定义的简单接口:ConsoleMessageWriterIMessageWriterIMessageWriter

To speak in DI terminology, we say that the IMessageWriter Dependency is injected into the Salutation class using a constructor argument. Note that Salutation has no awareness of ConsoleMessageWriter. It interacts with it exclusively through the IMessageWriter interface. IMessageWriter is a simple interface defined for the occasion:

public interface IMessageWriter
{
    void Write(string message);
}

它可能有其他成员,但在这个简单的示例中,您只需要Write方法。它由ConsoleMessageWriter类实现Main方法传递给Salutation类:

It could have had other members, but in this simple example, you only need the Write method. It’s implemented by the ConsoleMessageWriter class that the Main method passes to the Salutation class:

public class ConsoleMessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine(message);
    }
}

ConsoleMessageWriter类通过IMessageWriter包装Console类实现.NET 基类库(BCL)。这是我们在 1.1.2 节中谈到的适配器设计模式的简单应用。

The ConsoleMessageWriter class implements IMessageWriter by wrapping the Console class of the .NET Base Class Library (BCL). This is a simple application of the Adapter design pattern that we talked about in section 1.1.2.

1.2.2 DI 的好处

1.2.2 Benefits of DI

您可能想知道将一行代码替换为两个类和一个接口(总共 28 行)的好处。您可以轻松解决与此处所示相同的问题:

You may be wondering about the benefit of replacing a single line of code with two classes and an interface, resulting in 28 lines total. You could easily solve the same problem as shown here:

private static void Main()
{
    Console.WriteLine("Hello DI!");
}

DI 可能看起来有点矫枉过正,但使用它有几个好处。前面的示例与通常用于在 C# 中实现 Hello World 的单行代码相比有何优势?在此示例中,DI 增加了 2800% 的开销,但是,随着复杂性从一行代码增加到数万行,这种开销会减少甚至消失。第 3 章提供了一个更复杂的应用 DI 示例。尽管与现实生活中的应用程序相比,该示例仍然过于简单,但您应该注意到 DI 的侵入性要小得多。

DI might seem like overkill, but there are several benefits to be harvested from using it. How is the previous example better than the usual single line of code you normally use to implement Hello World in C#? In this example, DI adds an overhead of 2800%, but, as complexity increases from one line of code to tens of thousands, this overhead diminishes and all but disappears. Chapter 3 provides a more complex example of applied DI. Although that example is still overly simplistic compared to real-life applications, you should notice that DI is far less intrusive.

如果您发现前面的 DI 示例设计过度,我们不会责怪您,但考虑一下:就其本质而言,经典的 Hello World 示例是一个简单的问题,具有明确指定和约束的要求。在现实世界中,软件开发从来都不是这样的。需求不断变化,而且常常是模糊的。您必须实现的功能也往往要复杂得多。DI 通过启用松散耦合来帮助解决此类问题。具体来说,您将获得表 1.1中列出的好处。

We don’t blame you if you find the previous DI example to be over-engineered, but consider this: by its nature, the classic Hello World example is a simple problem with well-specified and constrained requirements. In the real world, software development is never like this. Requirements change and are often fuzzy. The features you must implement also tend to be much more complex. DI helps address such issues by enabling loose coupling. Specifically, you gain the benefits listed in table 1.1.

表 1.1 从松耦合中获得的好处。每项福利始终可用,但会根据情况进行不同的评估。
益处描述什么时候有价值?
后期绑定服务可以与其他服务交换而无需重新编译代码。在标准软件中很有价值,但在运行时环境往往定义良好的企业应用程序中可能就不那么重要了。
可扩展性可以以未明确计划的方式扩展和重用代码。永远有价值。
并行开发代码可以并行开发。在大型、复杂的应用程序中有价值;在小型、简单的应用程序中不是那么多。
可维护性职责明确的类更容易维护。永远有价值。
可测试性类可以进行单元测试。永远有价值。

我们首先列出了后期绑定的好处,因为根据我们的经验,这是大多数人心中最重要的好处。当架构师和开发人员未能理解松散耦合的好处时,很可能是因为他们从未考虑过其他好处。

We listed the late-binding benefit first because, in our experience, this is the one that’s foremost in most people’s minds. When architects and developers fail to understand the benefits of loose coupling, it’s most likely because they never consider the other benefits.

后期绑定

Late binding

当我们向接口和 DI 解释编程的好处时,将一种服务换成另一种服务的能力对大多数人来说是最显着的好处,因此他们往往只考虑这个好处来权衡利弊。还记得我们建议您在学习之前可能需要忘却吗?您可能会说您非常了解自己的需求,以至于您永远不必用其他任何东西替换,比如说,您的 SQL Server 数据库。但是要求会改变。

When we explain the benefits of programming to interfaces and DI, the ability to swap out one service with another is the most conspicuous benefit for most people, so they tend to weigh the advantages against the disadvantages with only this benefit in mind. Remember when we suggested that you may need to unlearn before you can learn? You may say that you know your requirements so well that you know you’ll never have to replace, say, your SQL Server database with anything else. But requirements change.

在 1.2.1 节中,您没有使用后期绑定,因为您通过对新实例的创建进行硬编码来显式创建了一个新实例。但是,您可以通过更改这一行代码来引入后期绑定:IMessageWriterConsoleMessageWriter

In section 1.2.1, you didn’t use late binding because you explicitly created a new instance of IMessageWriter by hard coding the creation of a new ConsoleMessageWriter instance. You can, however, introduce late binding by changing this single line of code:

IMessageWriter writer = new ConsoleMessageWriter();

要启用后期绑定,您可以将该行代码替换为类似以下内容。

To enable late binding, you might replace that line of code with something like the following.

清单 1.2 延迟绑定IMessageWriter实现

Listing 1.2 Late binding an IMessageWriter implementation

IConfigurationRoot configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

string typeName = configuration["messageWriter"];
Type type = Type.GetType(typeName, throwOnError: true);

IMessageWriter writer = (IMessageWriter)Activator.CreateInstance(type);

通过从应用程序配置文件中提取类型名称并创建一个Type实例从中,您可以使用反射创建一个实例,而无需在编译时知道具体类型。要使其工作,您在应用程序中指定类型名称IMessageWritermessageWriter在应用程序配置文件中设置:

By pulling the type name from the application configuration file and creating a Type instance from it, you can use reflection to create an instance of IMessageWriter without knowing the concrete type at compile time. To make this work, you specify the type name in the messageWriter application setting in the application configuration file:

{
  "messageWriter":
    "Ploeh.Samples.HelloDI.Console.ConsoleMessageWriter, HelloDI.Console"
}

松散耦合支持后期绑定,因为只有一个地方可以创建IMessageWriter. 因为Salutation专门针对IMessageWriter界面工作,它永远不会注意到差异。在你好 DI!例如,后期绑定将使您能够将消息写入与控制台不同的目的地;例如,数据库或文件。添加此类功能是可能的——即使您没有提前明确计划它们。

Loose coupling enables late binding because there’s only a single place where you create the instance of IMessageWriter. Because the Salutation class works exclusively against the IMessageWriter interface, it never notices the difference. In the Hello DI! example, late binding would enable you to write the message to a different destination than the console; for example, a database or a file. It’s possible to add such features — even though you didn’t explicitly plan ahead for them.

可扩展性

Extensibility

成功的软件必须能够改变。您需要添加新功能并扩展现有功能。松散耦合让您可以高效地重构应用程序,类似于您在使用电插头和插座时的灵活性。

Successful software must be able to change. You’ll need to add new features and extend existing features. Loose coupling lets you efficiently recompose the application, similar to the way you have flexibility when working with electrical plugs and sockets.

假设您想制作 Hello DI!通过仅允许经过身份验证的用户编写消息来更安全的示例。清单 1.3显示了如何在不更改任何现有功能的情况下添加该功能——您只需添加一个新的IMessageWriter接口实现。

Let’s say that you want to make the Hello DI! example more secure by only allowing authenticated users to write the message. Listing 1.3 shows how you can add that feature without changing any of the existing features — you simply add a new implementation of the IMessageWriter interface.

清单 1.3 扩展 Hello DI!应用具有安全功能

Listing 1.3 Extending the Hello DI! application with a security feature

public class SecureMessageWriter : IMessageWriter    ①  
{
    private readonly IMessageWriter writer;
    private readonly IIdentity identity;

    public SecureMessageWriter(
        IMessageWriter writer,    ②  
        IIdentity identity)
    {
        if (writer == null)
            throw new ArgumentNullException("writer");
        if (identity == null)
            throw new ArgumentNullException("identity");

        this.writer = writer;
        this.identity = identity;
    }

    public void Write(string message)
    {
        if (this.identity.IsAuthenticated)    ③  
        {
            this.writer.Write(message);    ④  
        }
    }
}

除了 的实例之外IMessageWriter,构造函数还需要 的实例。该方法是通过首先使用注入的. 如果是这种情况,它允许将修饰的编写器字段添加到消息中。唯一需要更改现有代码的地方是在方法中,因为您需要以不同于以前的方式组合可用的类:SecureMessageWriterIIdentityWriteIIdentityWriteMain

Besides an instance of IMessageWriter, the SecureMessageWriter constructor requires an instance of IIdentity. The Write method is implemented by first checking whether the current user is authenticated, using the injected IIdentity. If this is the case, it allows the decorated writer field to Write the message. The only place where you need to change existing code is in the Main method, because you need to compose the available classes differently than before:

IMessageWriter writer =
    new SecureMessageWriter(    ①  

        new ConsoleMessageWriter(),
        WindowsIdentity.GetCurrent());

请注意,您用新类包装或装饰了旧实例ConsoleMessageWriterSecureMessageWriter. 再一次,Salutation该类未被修改,因为它只使用IMessageWriter接口. 同样,无需修改或复制ConsoleWriter类中的功能, 任何一个。你使用System.Security.Principal.WindowsIdentity检索代表其执行此代码的用户的身份。10 

Notice that you wrap or decorate the old ConsoleMessageWriter instance with the new SecureMessageWriter class. Once more, the Salutation class is unmodified because it only consumes the IMessageWriter interface. Similarly, there’s no need to either modify or duplicate the functionality in the ConsoleWriter class, either. You use the System.Security.Principal.WindowsIdentity class to retrieve the identity of the user on whose behalf this code is being executed.10 

正如我们之前所述,松散耦合使您能够编写对可扩展性开放但对修改关闭的代码。唯一需要修改代码的地方是在应用程序入口点。实现应用程序的安全功能,同时处理用户界面。这使您能够独立地改变这些方面,并根据需要组合它们。每个类都有自己的单一职责SecureMessageWriterConsoleMessageWriter

As we’ve stated before, loose coupling enables you to write code that’s open for extensibility, but closed for modification. The only place where you need to modify the code is at the application entry point. SecureMessageWriter implements the security features of the application, whereas ConsoleMessageWriter addresses the user interface. This enables you to vary these aspects independently of each other and compose them as needed. Each class has its own Single Responsibility.

并行开发

Parallel development

关注点分离使得并行开发代码成为可能。当一个软件开发项目发展到一定规模时,就需要有多个开发人员在同一代码库上并行工作。在更大的规模上,甚至有必要将开发团队分成多个规模可管理的团队。每个团队通常被分配负责整个应用程序的一个区域。为了划分职责,每个团队开发一个或多个模块,这些模块需要集成到完成的应用程序中。除非每个团队的领域真正独立,否则某些团队可能会依赖其他团队开发的功能。

Separation of concerns makes it possible to develop code in parallel. When a software development project grows to a certain size, it becomes necessary to have multiple developers work in parallel on the same code base. At a larger scale, it’s even necessary to separate the development team into multiple teams of manageable sizes. Each team is often assigned responsibility for an area of the overall application. To demarcate responsibilities, each team develops one or more modules that will need to be integrated into the finished application. Unless the areas of each team are truly independent, some teams are likely to depend on functionality developed by other teams.

在前面的示例中,因为和类不直接相互依赖,所以它们可以由并行团队开发。他们需要就共享接口达成一致。SecureMessageWriterConsoleMessageWriterIMessageWriter

In the previous example, because the SecureMessageWriter and ConsoleMessageWriter classes don’t depend directly on each other, they could’ve been developed by parallel teams. All they would have needed to agree on was the shared interface IMessageWriter.

可维护性

Maintainability

随着每个类的职责得到明确定义和约束,整个应用程序的维护变得更加容易。这是单一职责原则的结果,它指出每个类应该只有一个责任。我们将在第 2 章中更详细地讨论单一职责原则

As the responsibility of each class becomes clearly defined and constrained, maintenance of the overall application becomes easier. This is a consequence of the Single Responsibility Principle, which states that each class should have only a single responsibility. We’ll discuss the Single Responsibility Principle in more detail in chapter 2.

向应用程序添加新功能变得更加简单,因为很清楚应该在何处应用更改。通常情况下,您不需要更改现有代码,而是可以添加新类并重构应用程序。这就是开闭原则再次行动。

Adding new features to an application becomes simpler because it’s clear where changes should be applied. More often than not, you don’t need to change existing code, but can instead add new classes and recompose the application. This is the Open/Closed Principle in action again.

故障排除也往往变得不那么费力,因为可能的罪魁祸首的范围缩小了。有了明确定义的职责,您通常会很清楚从哪里开始寻找问题的根本原因。

Troubleshooting also tends to become less grueling, because the scope of likely culprits narrows. With clearly defined responsibilities, you’ll often have a good idea of where to start looking for the root cause of a problem.

可测试性

Testability

当一个应用程序可以进行单元测试时,它被认为是可测试的。对于某些人来说,可测试性是他们最不担心的事情;对于其他人来说,这是绝对的要求。就个人而言,我们属于后一类。在 Mark 的职业生涯中,他拒绝了几份工作邀请,因为这些工作涉及使用某些不可测试的产品。

An application is considered Testable when it can be unit tested. For some, Testability is the least of their worries; for others, it’s an absolute requirement. Personally, we belong in the latter category. In Mark’s career, he’s declined several job offers because they involved working with certain products that weren’t Testable.

可测试性的好处可能是我们列出的那些中最具争议的。一些开发人员和架构师仍然不进行单元测试,因此他们充其量认为这种好处无关紧要。然而,我们将其视为软件开发的重要组成部分,这就是为什么我们在表 1.1中将其标记为“始终有价值” 。Michael Feathers 甚至定义了遗留应用程序这个术语与单元测试未涵盖的任何应用程序一样。11 

The benefit of Testability is perhaps the most controversial of those we’ve listed. Some developers and architects still don’t practice unit testing, so they consider this benefit irrelevant at best. We, however, see it as an essential part of software development, which is why we marked it as “Always valuable” in table 1.1. Michael Feathers even defines the term legacy application as any application that isn’t covered by unit tests.11 

几乎是偶然的,松散耦合使单元测试成为可能,因为消费者遵循里氏替换原则:他们不关心依赖项的具体类型。这意味着您可以将测试替身注入被测系统(被测物),正如您将在清单 1.4中看到的那样。

Almost by accident, loose coupling enables unit testing because consumers follow the Liskov Substitution Principle: they don’t care about the concrete types of their Dependencies. This means that you can inject Test Doubles into the System Under Test (SUT), as you’ll see in listing 1.4.

用特定于测试的替换替换预期的依赖项的能力是松散耦合的副产品,但我们选择将其列为单独的好处,因为派生值不同。我们的个人经验是,即使在集成测试期间,DI 也是有益的。尽管集成测试通常与真实的外部系统(如数据库)通信,但您仍然需要一定程度的隔离。换句话说,仍然有理由替换、拦截或模拟被测应用程序中的某些依赖项。

The ability to replace intended Dependencies with test-specific replacements is a by-product of loose coupling, but we chose to list it as a separate benefit because the derived value is different. Our personal experience is that DI is beneficial even during integration testing. Although integration tests typically communicate with real external systems (like a database), you still need to have a certain degree of isolation. In other words, there are still reasons to replace, Intercept, or mock certain Dependencies in the application being tested.

根据您正在开发的应用程序类型,您可能关心也可能不关心后期绑定的能力,但我们始终关心可测试性。一些开发人员不关心可测试性,但发现后期绑定对他们正在开发的应用程序很重要。无论如何,DI 以最小的额外开销提供了未来的选择。

Depending on the type of application you’re developing, you may or may not care about the ability to do late binding, but we always care about Testability. Some developers don’t care about Testability but find late binding important for the application they’re developing. Regardless, DI provides options in the future with minimal additional overhead today.

示例:单元测试 HelloDI 逻辑

Example: unit testing HelloDI logic

在 1.2.1 节中,您看到了 Hello DI!例子。虽然我们首先向您展示了最终代码,但我们是使用 TDD 开发的。清单 1.4显示了最重要的单元测试。

In section 1.2.1, you saw the Hello DI! example. Although we showed you the final code first, we developed it using TDD. Listing 1.4 shows the most important unit test.

清单 1.4 单元测试Salutation班级_

Listing 1.4 Unit testing the Salutation class

[Fact]
public void ExclaimWillWriteCorrectMessageToMessageWriter()
{
    var writer = new SpyMessageWriter();
    var sut = new Salutation(writer);    ①  
    sut.Exclaim();
    Assert.Equal(
        expected: "Hello DI!",
        actual: writer.WrittenMessage);
}

public class SpyMessageWriter : IMessageWriter
{
    public string WrittenMessage { get; private set; }

    public void Write(string message)
    {
        this.WrittenMessage += message;
    }
}

该类Salutation需要一个接口实例IMessageWriter,因此您需要创建一个。您可以使用任何实现,但在单元测试中,Test Double 可能很有用——在这种情况下,您可以使用自己的 Test Spy 实现。14 

The Salutation class needs an instance of the IMessageWriter interface, so you need to create one. You could use any implementation, but in unit tests, a Test Double can be useful — in this case, you roll your own Test Spy implementation.14 

在这种情况下,测试替身与生产实施一样复杂。这是我们的示例多么简单的产物。在大多数应用程序中,测试替身比它所代表的具体的生产实现要简单得多。重要的部分是提供特定于测试的实现,IMessageWriter以确保您一次只测试一件事。现在,您正在测试该Exclaim方法Salutation班级的,因此您不希望 的生产实施IMessageWriter污染测试。要创建该类,请传入使用构造函数注入Salutation的 Test Spy 实例IMessageWriter.

In this case, the Test Double is as involved as the production implementation. This is an artifact of how simple our example is. In most applications, a Test Double is significantly simpler than the concrete, production implementations it stands in for. The important part is to supply a test-specific implementation of IMessageWriter to ensure that you test only one thing at a time. Right now, you’re testing the Exclaim method of the Salutation class, so you don’t want a production implementation of IMessageWriter to pollute the test. To create the Salutation class, you pass in the Test Spy instance of IMessageWriter using Constructor Injection.

行使 SUT 后,您可以调用以验证预期结果是否等于实际结果。如果方法Assert.EqualIMessageWriter.Write被调用了“Hello DI!” 字符串,会把它存储在它的属性中SpyMessageWriterWrittenMessageEqual方法完成。但是,如果该Write方法未被调用,或以不同的值被调用,则该Equal方法会抛出异常,测试会失败。

After exercising the SUT, you can call Assert.Equal to verify whether the expected outcome equals the actual outcome. If the IMessageWriter.Write method was invoked with the "Hello DI!" string, SpyMessageWriter would have stored this in its WrittenMessage property, and the Equal method completes. But if the Write method wasn’t called, or was called with a different value, the Equal method would throw an exception, and the test would fail.

松散耦合提供了许多好处:代码变得更易于开发、维护和扩展,并且变得更易于测试。这甚至不是特别困难。我们针对接口而不是具体实现进行编程。唯一的主要障碍是弄清楚如何获得这些接口的实例。DI 通过从外部注入依赖项来克服这个障碍。构造函数注入是实现这一点的首选方法,尽管我们还将在第 4 章中探索一些额外的选项。

Loose coupling provides many benefits: code becomes easier to develop, maintain, and extend, and it becomes more Testable. It’s not even particularly difficult. We program against interfaces, not concrete implementations. The only major obstacle is to figure out how to get hold of instances of those interfaces. DI surmounts this obstacle by injecting the Dependencies from the outside. Constructor Injection is the preferred method of doing that, though we’ll also explore a few additional options in chapter 4.

1.3 注入什么和不注入什么

1.3 What to inject and what not to inject

在上一节中,我们首先描述了让人想到 DI 的动机。如果您确信松散耦合是一种好处,那么您可能希望使所有内容都松散耦合。总的来说,这是个好主意。当您需要决定如何打包模块时,松散耦合被证明特别有用。但是您不必将所有内容都抽象出来并使其可插入。在本节中,我们将提供一些决策工具来帮助您决定如何对依赖项建模。

In the previous section, we described the motivational forces that makes one think about DI in the first place. If you’re convinced that loose coupling is a benefit, you may want to make everything loosely coupled. Overall, that’s a good idea. When you need to decide how to package modules, loose coupling proves especially useful. But you don’t have to abstract everything away and make it pluggable. In this section, we’ll provide some decision tools to help you decide how to model your Dependencies.

.NET BCL 由许多程序集组成。每次编写使用 BCL 程序集类型的代码时,都会向模块添加依赖项。在上一节中,我们讨论了松散耦合的重要性以及接口编程的基石。这是否意味着您不能引用任何 BCL 程序集并直接在您的应用程序中使用它们的类型?如果您想使用在 System.Xml 程序集中定义的XmlWriter

The .NET BCL consists of many assemblies. Every time you write code that uses a type from a BCL assembly, you add a dependency to your module. In the previous section, we discussed how loose coupling is important and how programming to an interface is the cornerstone. Does this imply that you can’t reference any BCL assemblies and use their types directly in your application? What if you’d like to use an XmlWriter that’s defined in the System.Xml assembly?

您不必平等对待所有依赖项。BCL 中的许多类型都可以在不损害应用程序耦合度的情况下使用——但不是全部。重要的是要知道如何区分不会造成危险的类型和可能会加强应用程序耦合度的类型。主要关注后者。

You don’t have to treat all Dependencies equally. Many types in the BCL can be used without jeopardizing an application’s degree of coupling — but not all of them. It’s important to know how to distinguish between types that pose no danger and types that may tighten an application’s degree of coupling. Focus mainly on the latter.

在学习 DI 时,将依赖项分类为稳定依赖项和易变依赖项会很有帮助。决定将接缝放在哪里将很快成为您的第二天性。下一节将更详细地讨论这些概念。

As you learn DI, it can be helpful to categorize your Dependencies into Stable Dependencies and Volatile Dependencies. Deciding where to put your Seams will soon become second nature to you. The next sections discuss these concepts in more detail.

1.3.1 稳定的依赖

1.3.1 Stable Dependencies

BCL 中的许多模块及以上不会对应用程序的模块化程度构成威胁。它们包含可重用的功能,您可以使用这些功能使自己的代码更加简洁。BCL 模块对您的应用程序始终可用,因为它需要 .NET Framework 才能运行,并且因为它们已经存在,所以对并行开发的关注不适用于这些模块。您始终可以在另一个应用程序中重用 BCL 库。

Many of the modules in the BCL and beyond pose no threat to an application’s degree of modularity. They contain reusable functionality that you can use to make your own code more succinct. The BCL modules are always available to your application, because it needs the .NET Framework to run, and, because they already exist, the concern about parallel development doesn’t apply to these modules. You can always reuse a BCL library in another application.

默认情况下,您可以将 BCL 中定义的大多数(但不是全部)类型视为安全或稳定的依赖项。我们称它们为稳定的,因为它们已经存在,它们往往是向后兼容的,并且调用它们具有确定性的结果。大多数稳定依赖项都是 BCL 类型,但其他依赖项也可以是稳定的。稳定依赖关系的重要标准包括以下内容:

By default, you can consider most (but not all) types defined in the BCL as safe, or Stable Dependencies. We call them stable because they’re already there, they tend to be backward compatible, and invoking them has deterministic outcomes. Most Stable Dependencies are BCL types, but other Dependencies can be stable too. The important criteria for Stable Dependencies include the following:

  • 类或模块已经存在。
  • The class or module already exists.
  • 您希望新版本不会包含重大更改。
  • You expect that new versions won’t contain breaking changes.
  • 所讨论的类型包含确定性算法。
  • The types in question contain deterministic algorithms.
  • 您永远不会期望必须用另一个替换、包装、装饰或拦截类或模块。
  • You never expect to have to replace, wrap, decorate, or Intercept the class or module with another.

其他示例可能包括封装与您的应用程序相关的算法的专用库。例如,如果您正在开发处理化学的应用程序,您可以引用包含化学特定功能的第三方库。

Other examples may include specialized libraries that encapsulate algorithms relevant to your application. For example, if you’re developing an application that deals with chemistry, you can reference a third-party library that contains chemistry-specific functionality.

一般来说,Dependencies可以被认为是稳定的。如果它们不不稳定,它们就是稳定的。

In general, Dependencies can be considered stable by exclusion. They’re stable if they aren’t volatile.

1.3.2 不稳定的依赖

1.3.2 Volatile Dependencies

将Seams引入应用程序是一项额外的工作,因此您应该只在必要时才这样做。可能有不止一个原因需要隔离Seam后面的依赖关系,但这些原因与松散耦合的好处密切相关(在第 1.2.1 节中讨论)。

Introducing Seams into an application is extra work, so you should only do it when it’s necessary. There can be more than one reason it’s necessary to isolate a Dependency behind a Seam, but those reasons are closely related to the benefits of loose coupling (discussed in section 1.2.1).

这种依赖性可以通过它们干扰这些好处中的一项或多项的倾向来识别。它们不稳定,因为它们没有提供足够的应用程序的基础,因此我们称它们为易失性依赖项。如果满足以下任何条件,则应将依赖项视为易变的

Such Dependencies can be recognized by their tendency to interfere with one or more of these benefits. They aren’t stable because they don’t provide a sufficient foundation for applications, and we call them Volatile Dependencies for that reason. A Dependency should be considered volatile if any of the following criteria are true:

  • 依赖项引入了为应用程序设置和配置运行时环境的要求。易变的与其说是具体的 .NET 类型,不如说是它们对运行时环境的暗示。

    数据库是 BCL 类型的很好的例子,它们是Volatile Dependencies,而关系数据库是典型的例子。如果你不在Seam背后隐藏一个关系数据库,你就永远无法用任何其他技术取代它。它还使设置和运行自动化单元测试变得困难。(尽管 Microsoft SQL Server 客户端库是 BCL 中包含的一项技术,但它的使用暗示了关系数据库。)其他进程外资源,如消息队列、Web 服务,甚至文件系统都属于这一类。这种类型的依赖的症状是缺乏后期绑定和可扩展性,以及禁用的可测试性。

  • The Dependency introduces a requirement to set up and configure a runtime environment for the application. It isn’t so much the concrete .NET types that are volatile, but rather what they imply about the runtime environment.

    Databases are good examples of BCL types that are Volatile Dependencies, and relational databases are the archetypical example. If you don’t hide a relational database behind a Seam, you can never replace it by any other technology. It also makes it hard to set up and run automated unit tests. (Even though the Microsoft SQL Server client library is a technology contained in the BCL, its usage implies a relational database.) Other out-of-process resources like message queues, web services, and even the filesystem fall into this category. The symptoms of this type of Dependency are lack of late binding and extensibility, as well as disabled Testability.

  • 依赖项尚不存在,或者仍在开发中。
  • The Dependency doesn’t yet exist, or is still in development.
  • 依赖项并未安装在开发组织的所有计算机上。对于无法在所有操作系统上安装的昂贵的第三方库或依赖项,可能就是这种情况。最常见的症状是禁用可测试性。
  • The Dependency isn’t installed on all machines in the development organization. This may be the case for expensive third-party libraries or Dependencies that can’t be installed on all operating systems. The most common symptom is disabled Testability.
  • Dependency包含不确定的行为。这在单元测试中尤为重要,因为所有测试都必须是确定性的。不确定性的典型来源是随机数和依赖于当前日期或时间的算法。

    因为 BCL 定义了不确定性的常见来源,例如、或,所以您无法避免引用定义它们的程序集。尽管如此,您应该将它们视为易失性依赖项,因为它们往往会破坏可测试性。System.RandomSystem.Security.Cryptography.RandomNumberGeneratorSystem.DateTime.Now

  • The Dependency contains nondeterministic behavior. This is particularly important in unit tests because all tests must be deterministic. Typical sources of nondeterminism are random numbers and algorithms that depend on the current date or time.

    Because the BCL defines common sources of nondeterminism, such as System.Random, System.Security.Cryptography.RandomNumberGenerator, or System.DateTime.Now, you can’t avoid having a reference to the assembly in which they’re defined. Nevertheless, you should treat them as Volatile Dependencies because they tend to destroy Testability.

现在您了解了稳定依赖项和可变依赖项之间的区别,您可以开始了解 DI 范围的轮廓。松散耦合是一种普遍的设计原则,因此 DI(作为推动者)应该在您的代码库中无处不在。DI 的主题和良好的软件设计之间没有严格的界限,但是为了定义本书其余部分的范围,我们将快速描述它所涵盖的内容。

Now that you understand the differences between Stable and Volatile Dependencies, you can begin to see the contours of the scope of DI. Loose coupling is a pervasive design principle, so DI (as an enabler) should be everywhere in your code base. There’s no hard line between the topic of DI and good software design, but to define the scope of the rest of the book, we’ll quickly describe what it covers.

1.4 DI范围

1.4 DI scope

正如我们之前所讨论的,DI 的一个重要元素是将各种职责分解为单独的类。我们从课堂上带走的一项责任是创建Dependencies的实例。创建Dependencies实例的任务称为Object Composition

As we discussed before, an important element of DI is to break up various responsibilities into separate classes. One responsibility that we take away from classes is the task of creating instances of Dependencies. The task of creating instances of Dependencies is referred to as Object Composition.

我们在 Hello DI 中讨论过这个问题!例如,我们的Salutation班级被释放了创建其Dependency的责任。相反,这个责任被转移到应用程序的Main方法. UML 图再次显示在图 1.11中。

We discussed this in our Hello DI! example where our Salutation class was released of the responsibility of creating its Dependency. Instead, this responsibility was moved to the application’s Main method. The UML diagram is shown again in figure 1.11.

01-12.eps

图 1.11 Hello DI! 合作者关系图 应用(重复)

Figure 1.11 Relationship between the collaborators of the Hello DI! application (repeated)

当一个类放弃对Dependencies的控制时,它放弃的不仅仅是选择特定实现的决定。通过这样做,我们作为开发人员可以获得一些优势。乍一看,让类交出对创建哪些对象的控制权似乎是一种劣势,但我们并没有失去这种控制权——我们只是将它移到了另一个地方。

As a class relinquishes control of Dependencies, it gives up more than the decision to select particular implementations. By doing this, we, as developers, gain some advantages. At first, it may seem like a disadvantage to let a class surrender control over which objects are created, but we don’t lose that control — we only move it to another place.

对象组合不是我们删除的唯一控制维度:类也失去了控制对象生命周期的能力。当一个Dependency实例被注入到一个类中时,消费者不知道它是什么时候创建的,也不知道它什么时候会超出范围。这对消费者来说应该是无关紧要的。让消费者忘记其依赖项的生命周期可以简化消费者。

Object Composition isn’t the only dimension of control that we remove: a class also loses the ability to control the lifetime of the object. When a Dependency instance is injected into a class, the consumer doesn’t know when it was created, or when it’ll go out of scope. This should be of no concern to the consumer. Making the consumer oblivious to the lifetime of its Dependencies simplifies the consumer.

DI 使您有机会以统一的方式管理依赖项。当消费者直接创建和设置Dependencies的实例时,每个人都可以以自己的方式进行。这可能与其他消费者的做法不一致。您没有办法集中管理依赖关系,也没有简单的方法来解决跨领域问题. 使用 DI,您可以获得拦截每个依赖实例并在它传递给消费者之前对其进行操作的能力。这提供了应用程序的可扩展性。

DI gives you an opportunity to manage Dependencies in a uniform way. When consumers directly create and set up instances of Dependencies, each may do so in its own way. This can be inconsistent with how other consumers do it. You have no way to centrally manage Dependencies and no easy way to address Cross-Cutting Concerns. With DI, you gain the ability to Intercept each Dependency instance and act on it before it’s passed to the consumer. This provides extensibility in applications.

使用 DI,您可以在拦截依赖项并控制其生命周期的同时编写应用程序。Object CompositionInterceptionLifetime Management是 DI 的三个维度。接下来,我们将简要介绍其中的每一个;本书的第 3 部分将进行更详细的处理。

With DI, you can compose applications while intercepting Dependencies and controlling their lifetimes. Object Composition, Interception, and Lifetime Management are three dimensions of DI. Next, we’ll cover each of these briefly; a more detailed treatment follows in part 3 of the book.

1.4.1 对象组合

1.4.1 Object Composition

要获得可扩展性、后期绑定和并行开发的好处,您必须能够将类组合到应用程序中。这意味着您需要通过将各个类放在一起来创建一个应用程序,就像将电器连接在一起一样。而且,与电器一样,您会希望在引入新要求时轻松地重新排列这些类别,理想情况下无需对现有类别进行更改。

To harvest the benefits of extensibility, late binding, and parallel development, you must be able to compose classes into applications. This means that you’ll want to create an application out of individual classes by putting them together, much like plugging electrical appliances together. And, as with electrical appliances, you’ll want to easily rearrange those classes when new requirements are introduced, ideally, without having to make changes to existing classes.

对象组合通常是将 DI 引入应用程序的主要动机。事实上,最初,DI 是Object Composition的同义词;这是 Martin Fowler 关于该主题的原始文章中讨论的唯一方面。16 

Object Composition is often the primary motivation for introducing DI into an application. In fact, initially, DI was synonymous with Object Composition; it’s the only aspect discussed in Martin Fowler’s original article on the subject.16 

您可以通过多种方式将类组合到应用程序中。当我们讨论后期绑定时,我们使用一个配置文件和一些动态对象实例化来从可用模块手动组合应用程序。我们也可以使用DI Container使用Configuration as Code。我们将在第 12 章回到这些。

You can compose classes into an application in several ways. When we discussed late binding, we used a configuration file and a bit of dynamic object instantiation to manually compose the application from the available modules. We could also have used Configuration as Code using a DI Container. We’ll return to these in chapter 12.

许多人将 DI 称为控制反转(国际奥委会)。这两个术语有时可以互换使用,但 DI 是 IoC 的一个子集。在整本书中,我们始终使用最具体的术语——DI。如果我们指的是 IoC,我们会专门提到它。

Many people refer to DI as Inversion of Control (IoC). These two terms are sometimes used interchangeably, but DI is a subset of IoC. Throughout the book, we consistently use the most specific term — DI. If we mean IoC, we refer to it specifically.

1.4.2 对象生命周期

1.4.2 Object Lifetime

放弃对其依赖项的控制的类放弃的不仅仅是选择抽象的特定实现的权力。它还放弃了控制实例何时创建以及何时超出范围的权力。

A class that has surrendered control of its Dependencies gives up more than the power to select particular implementations of an Abstraction. It also gives up the power to control when instances are created and when they go out of scope.

在 .NET 中,垃圾收集器为我们处理这些事情。消费者可以将其依赖项注入其中并根据需要使用它们。完成后,依赖项超出范围。如果没有其他类引用它们,则它们有资格进行垃圾回收。

In .NET, the garbage collector takes care of these things for us. A consumer can have its Dependencies injected into it and use them for as long as it wants. When it’s done, the Dependencies go out of scope. If no other classes reference them, they’re eligible for garbage collection.

如果两个消费者共享相同类型的Dependency怎么办?清单 1.5说明您可以选择向每个消费者注入一个单独的实例,而清单 1.6表明您可以选择在多个消费者之间共享一个实例。但从消费者的角度来看,没有区别。根据Liskov 替换原则,消费者必须平等对待给定接口的所有实例。

What if two consumers share the same type of Dependency? Listing 1.5 illustrates that you can choose to inject a separate instance into each consumer, whereas listing 1.6 shows that you can alternatively choose to share a single instance across several consumers. But from the perspective of the consumer, there’s no difference. According to the Liskov Substitution Principle, the consumer must treat all instances of a given interface equally.

清单 1.5 消费者获取自己的同类型依赖实例

Listing 1.5 Consumers getting their own instance of the same type of Dependency

IMessageWriter writer1 = new ConsoleMessageWriter();    ①  
IMessageWriter writer2 = new ConsoleMessageWriter();    ①  

var salutation = new Salutation(writer1);    ②  
var valediction = new Valediction(writer2);    ②  

清单 1.6 共享同类型依赖实例的消费者

Listing 1.6 Consumers sharing an instance of the same type of Dependency

IMessageWriter writer = new ConsoleMessageWriter();    ①  

var salutation = new Salutation(writer);    ②  
var valediction = new Valediction(writer);    ②  

因为可以共享依赖关系,所以单个消费者不可能控制其生命周期。只要托管对象可以超出范围并被垃圾收集,这就不是什么大问题。但是当Dependencies实现IDisposable接口时,事情变得更加复杂,我们将在 8.2 节中讨论。总的来说,终身管理是 DI 的一个独立维度,并且非常重要,我们将第 8 章的所有内容都放在一边。

Because Dependencies can be shared, a single consumer can’t possibly control its lifetime. As long as a managed object can go out of scope and be garbage collected, this isn’t much of an issue. But when Dependencies implement the IDisposable interface, things become much more complicated as we’ll discuss in section 8.2. As a whole, Lifetime Management is a separate dimension of DI and important enough that we’ve set aside all of chapter 8 for it.

1.4.3 拦截

1.4.3 Interception

当我们将对依赖项的控制委托给第三方时,如图 1.12所示,我们还提供了在将它们传递给使用它们的类之前修改它们的能力。

When we delegate control over Dependencies to a third party, as figure 1.12 shows, we also provide the power to modify them before we pass them on to the classes consuming them.

01-13.eps

图 1.12 拦截 aConsoleMessageWriter

Figure 1.12 Intercepting a ConsoleMessageWriter

在你好 DI!例如,我们最初将一个实例注入到一个实例中。然后,修改示例,我们添加了一个安全功能,方法是创建一个新的,仅在用户通过身份验证时将进一步的工作委托给。这允许您维护单一职责原则ConsoleMessageWriterSalutationSecureMessageWriterConsoleMessageWriter. 这样做是可能的,因为您总是针对接口进行编程;回想一下Dependencies必须始终是Abstractions。对于 the Salutation,它不关心提供IMessageWriter的是 aConsoleMessageWriter还是 a SecureMessageWriterSecureMessageWritercan wrapConsoleMessageWriter仍然执行实际工作。

In the Hello DI! example, we initially injected a ConsoleMessageWriter instance into a Salutation instance. Then, modifying the example, we added a security feature by creating a new SecureMessageWriter that only delegates further work to the ConsoleMessageWriter when the user is authenticated. This allows you to maintain the Single Responsibility Principle. It’s possible to do this because you always program to interfaces; recall that Dependencies must always be Abstractions. In the case of the Salutation, it doesn’t care whether the supplied IMessageWriter is a ConsoleMessageWriter or a SecureMessageWriter. The SecureMessageWriter can wrap a ConsoleMessageWriter that still performs the real work.

拦截的这种能力使我们沿着面向方面编程的道路前进(AOP),我们将在第 10 章和第 11 章中讨论一个密切相关的主题。使用拦截和 AOP,您可以应用横切关注点例如日志记录、审计、访问控制、验证等,以一种结构良好的方式进行,让您保持关注点分离。

Such abilities of Interception move us along the path towards Aspect-Oriented Programming (AOP), a closely related topic that we’ll cover in chapters 10 and 11. With Interception and AOP, you can apply Cross-Cutting Concerns such as logging, auditing, access control, validation, and so forth in a well-structured manner that lets you maintain Separation of Concerns.

1.4.4 三维中的 DI

1.4.4 DI in three dimensions

尽管 DI 最初是一系列旨在解决对象组合问题的模式,但该术语随后扩展到还涵盖对象生命周期拦截。今天,我们认为 DI 以一致的方式包含所有这三个方面。

Although DI started out as a series of patterns aimed at solving the problem of Object Composition, the term has subsequently expanded to also cover Object Lifetime and Interception. Today, we think of DI as encompassing all three in a consistent way.

Object Composition往往会占据主导地位,因为如果没有灵活的Object Composition,就没有Interception也不需要管理Object Lifetime对象组合主导了本章的大部分内容,并将继续主导本书,但你不应该忘记其他方面。对象组合提供了基础,生命周期管理解决了一些重要的副作用。但主要是当涉及到拦截时,您才开始收获好处。

Object Composition tends to dominate the picture because, without flexible Object Composition, there’d be no Interception and no need to manage Object Lifetime. Object Composition has dominated most of this chapter and will continue to dominate this book, but you shouldn’t forget the other aspects. Object Composition provides the foundation, and Lifetime Management addresses some important side effects. But it’s mainly when it comes to Interception that you start to reap the benefits.

在第 3 部分中,我们用一章专门介绍了这里简要提到的每个维度。但重要的是要知道,在实践中,DI 不仅仅是Object Composition

In part 3, we’ve devoted a chapter to each dimension briefly mentioned here. But it’s important to know that, in practice, DI is more than Object Composition.

1.5 结论

1.5 Conclusion

依赖注入是达到目的的手段,而不是目标本身。这是实现松耦合的最佳方式,松耦合是可维护代码的重要组成部分。从松散耦合中获得的好处并不总是立即显现出来,但随着代码库复杂性的增加,它们会随着时间的推移变得明显。与 DI 相关的松散耦合的一个重点是,为了有效,它应该在代码库中无处不在。

Dependency Injection is a means to an end, not a goal in itself. It’s the best way to enable loose coupling, an important part of maintainable code. The benefits you can reap from loose coupling aren’t always immediately apparent, but they’ll become visible over time, as the complexity of a code base grows. An important point about loose coupling in relation to DI is that, in order to be effective, it should be everywhere in your code base.

紧密耦合的代码库最终会退化为意大利面条代码;18  而设计良好、松散耦合的代码库可以保持可维护性。实现真正灵活的设计需要的不仅仅是松散耦合,19  但接口编程是先决条件。

A tightly coupled code base will eventually deteriorate into Spaghetti Code;18  whereas a well-designed, loosely coupled code base can stay maintainable. It takes more than loose coupling to reach a truly supple design,19  but programming to interfaces is a prerequisite.

DI 只不过是设计原则和模式的集合。它更多地是关于一种思考和设计代码的方式,而不是关于工具和技术。DI 的目的是使代码可维护。小型代码库,如经典的 Hello World 示例,由于其大小而具有内在的可维护性。这就是为什么 DI 在简单的例子中往往看起来像过度工程。代码库越大,好处就越明显。我们在接下来的两章中专门介绍了一个更大、更复杂的示例来展示这些好处。

DI is nothing more than a collection of design principles and patterns. It’s more about a way of thinking and designing code than it is about tools and techniques. The purpose of DI is to make code maintainable. Small code bases, like a classic Hello World example, are inherently maintainable because of their size. This is why DI tends to look like overengineering in simple examples. The larger the code base becomes, the more visible the benefits. We’ve dedicated the next two chapters to a larger and more complex example to showcase these benefits.

概括

Summary

  • 依赖注入是一组软件设计原则和模式,使您能够开发松散耦合的代码。松散耦合使代码更易于维护。
  • Dependency Injection is a set of software design principles and patterns that enables you to develop loosely coupled code. Loose coupling makes code more maintainable.
  • 当您拥有松散耦合的基础架构时,任何人都可以使用它并适应不断变化的需求和意外需求,而无需对应用程序的代码库及其基础架构进行大量更改。
  • When you have a loosely coupled infrastructure in place, it can be used by anyone and adapted to changing needs and unanticipated requirements without having to make large changes to the application’s code base and its infrastructure.
  • 故障排除往往会变得不那么费力,因为可能的罪魁祸首的范围缩小了。
  • Troubleshooting tends to become less taxing because the scope of likely culprits narrows.
  • DI 支持后期绑定,即无需重新编译原始代码即可将类或模块替换为不同类或模块的能力。
  • DI enables late binding, which is the ability to replace classes or modules with different ones without the need for the original code to be recompiled.
  • DI 使代码更容易以未明确计划的方式扩展和重用,类似于您在使用电插头和插座时具有灵活性的方式。
  • DI makes it easier for code to be extended and reused in ways not explicitly planned for, similar to the way you have flexibility when working with electrical plugs and sockets.
  • DI简化了同一代码库的并行开发,因为关注点分离允许每个团队成员甚至整个团队更轻松地处理孤立的部分。
  • DI simplifies parallel development on the same code base because the Separation of Concerns allows each team member or even entire teams to work more easily on isolated parts.
  • DI 使软件更易于测试,因为您可以在编写单元测试时将依赖项替换为测试实现。
  • DI makes software more Testable because you can replace Dependencies with test implementations when writing unit tests.
  • 当你练习 DI 时,协作类应该依赖基础设施来提供必要的服务。你可以通过让你的类依赖于接口而不是具体的实现来做到这一点。
  • When you practice DI, collaborating classes should rely on infrastructure to provide the necessary services. You do this by letting your classes depend on interfaces, instead of concrete implementations.
  • 类不应该向第三方询问它们的Dependencies这是一种称为服务定位器的反模式。相反,类应该使用构造函数参数静态指定它们所需的依赖项,这种做法称为构造函数注入
  • Classes shouldn’t ask a third party for their Dependencies. This is an anti-pattern called Service Locator. Instead, classes should specify their required Dependencies statically using constructor parameters, a practice called Constructor Injection.
  • 许多开发人员认为 DI 需要专门的工具,即所谓的DI 容器。这是一个神话。DI 容器是一个有用但可选的工具。
  • Many developers think that DI requires specialized tooling, a so-called DI Container. This is a myth. A DI Container is a useful, but optional, tool.
  • 支持 DI 的最重要的软件设计原则之一是Liskov 替换原则。它允许在不破坏客户端或实现的情况下将接口的一种实现替换为另一种实现。
  • One of the most important software design principles that enables DI is the Liskov Substitution Principle. It allows replacing one implementation of an interface with another without breaking either the client or the implementation.
  • 如果依赖项已经可用、具有确定性行为、不需要设置运行时环境(例如关系数据库)并且不需要被替换、包装或拦截,则它们被认为是稳定的。
  • Dependencies are considered Stable in the case that they’re already available, have deterministic behavior, don’t require a setup runtime environment (such as a relational database), and don’t need to be replaced, wrapped, or intercepted.
  • 依赖项在开发过程中被认为是易变的,并不总是在所有开发机器上可用,包含不确定的行为,或者需要被替换、包装或拦截。
  • Dependencies are considered Volatile when they are under development, aren’t always available on all development machines, contain nondeterministic behavior, or need to be replaced, wrapped, or intercepted.
  • 易失性依赖项是 DI 的焦点。我们将Volatile Dependencies注入到类的构造函数中。
  • Volatile Dependencies are the focal point of DI. We inject Volatile Dependencies into a class’s constructor.
  • 通过从消费者那里移除对依赖项的控制,并将该控制权移至应用程序入口点,您可以获得更轻松地应用横切关注点的能力,并且可以更有效地管理依赖项的生命周期。
  • By removing control over Dependencies from their consumers, and moving that control into the application entry point, you gain the ability to apply Cross-Cutting Concerns more easily and can manage the lifetime of Dependencies more effectively.
  • 要取得成功,您需要普遍应用 DI。所有都应该使用Constructor Injection获得所需的Volatile Dependencies。很难将松散耦合和 DI 改造到现有代码库中。
  • To succeed, you need to apply DI pervasively. All classes should get their required Volatile Dependencies using Constructor Injection. It’s hard to retrofit loose coupling and DI onto an existing code base.

2

编写紧耦合代码

2

Writing tightly coupled code

在这一章当中

In this chapter

  • 编写紧密耦合的应用程序
  • Writing a tightly coupled application
  • 评估该应用程序的可组合性
  • Evaluating the composability of that application
  • 分析该应用程序中缺乏可组合性
  • Analyzing the lack of composability in that application

正如我们在第 1 章中提到的,蛋黄酱是一种由蛋黄和黄油制成的乳化酱汁,但这并不能神奇地让你具备制作蛋黄酱的能力。最好的学习方法是实践,但一个例子往往可以弥合理论与实践之间的差距。在您自己尝试之前,先观看专业厨师制作蛋黄酱酱会很有帮助。

As we mentioned in chapter 1, a sauce béarnaise is an emulsified sauce made from egg yolk and butter, but this doesn’t magically instill in you the ability to make one. The best way to learn is to practice, but an example can often bridge the gap between theory and practice. Watching a professional cook making a sauce béarnaise is helpful before you try it out yourself.

当我们在上一章介绍依赖注入 (DI) 时,我们提供了一个高级导览来帮助您理解它的目的和一般原则。但这个简单的解释并不能公正地对待 DI。DI 是一种启用松耦合的方法,而松耦合首先是一种处理复杂性的有效方法。

When we introduced Dependency Injection (DI) in the last chapter, we presented a high-level tour to help you understand its purpose and general principles. But that simple explanation doesn’t do justice to DI. DI is a way to enable loose coupling, and loose coupling is first and foremost an efficient way to deal with complexity.

大多数软件都是复杂的,因为它必须同时解决许多问题。除了本身可能很复杂的业务问题外,软件还必须解决与安全、诊断、操作、性能和可扩展性相关的问题。松耦合鼓励您单独解决每个问题,而不是在一个大泥球中解决所有这些问题。单独解决每个问题更容易,但最终,您仍然必须将这组复杂的问题组合到一个应用程序中。

Most software is complex in the sense that it must address many issues simultaneously. Besides the business concerns, which may be complex in their own right, software must also address matters related to security, diagnostics, operations, performance, and extensibility. Instead of addressing all of these concerns in one big ball of mud, loose coupling encourages you to address each concern separately. It’s easier to address each in isolation, but ultimately, you must still compose this complex set of issues into a single application.

在本章中,我们将看一个更复杂的例子。您将看到编写紧密耦合的代码是多么容易。您还将与我们一起从可维护性的角度分析为什么紧密耦合的代码会出现问题。在第 3 章中,我们将使用 DI 将这一紧耦合的代码库完全重写为松耦合的代码库。如果你想马上看到松散耦合的代码,你可能想跳过这一章。如果没有,当你读完本章后,你应该开始理解是什么导致紧耦合代码如此成问题。

In this chapter, we’ll take a look at a more complex example. You’ll see how easy it is to write tightly coupled code. You’ll also join us in an analysis of why tightly coupled code is problematic from a maintainability perspective. In chapter 3, we’ll use DI to completely rewrite this tightly coupled code base to one that’s loosely coupled. If you want to see loosely coupled code right away, you may want to skip this chapter. If not, when you’re done with this chapter, you should begin to understand what it is that makes tightly coupled code so problematic.

2.1 构建紧耦合应用

2.1 Building a tightly coupled application

构建松散耦合代码的想法并不是特别有争议,但理论与实践之间存在巨大差距。在下一章向您展示如何使用 DI 构建松散耦合的应用程序之前,我们想向您展示它是多么容易出错。松散耦合代码的常见尝试是构建分层应用程序。任何人都可以画出三层应用图,图2.1证明我们也可以。

The idea of building loosely coupled code isn’t particularly controversial, but there’s a huge gap between theory and practice. Before we show you in the next chapter how to use DI to build a loosely coupled application, we want to show you how easily it can go wrong. A common attempt at loosely coupled code is building a layered application. Anyone can draw a three-layer application diagram, and figure 2.1 proves that we can too.

02-01.eps

图 2.1 标准的三层应用架构。这是n层应用程序体系结构的最简单和最常见的变体,其中应用程序由n 个不同的层组成。

Figure 2.1 Standard three-layer application architecture. This is the simplest and most common variation of the n-layer application architecture, whereby an application is composed of n distinct layers.

绘制三层图看似简单,但绘制图表的行为类似于声明您将在牛排中加入蛋黄酱:这是一种意图声明,不保证最终结果。你很快就会看到,你可以得到别的东西。

Drawing a three-layer diagram is deceptively simple, but the act of drawing the diagram is akin to stating that you’ll have sauce béarnaise with your steak: it’s a declaration of intent that carries no guarantee with regard to the final result. You can end up with something else, as you shall soon see.

查看和设计灵活且可维护的复杂应用程序的方法不止一种,但是n应用程序架构构成了一种众所周知的、久经考验的方法。挑战在于正确实施它。有了如图 2.1 所示的三层图,您就可以开始构建应用程序了。

There’s more than one way to view and design a flexible and maintainable complex application, but the n-layer application architecture constitutes a well-known, tried-and-tested approach. The challenge is to implement it correctly. Armed with a three-layer diagram like the one in figure 2.1, you can start building an application.

2.1.1 认识玛丽罗文

2.1.1 Meet Mary Rowan

Mary Rowan 是一名专业的 .NET 开发人员,就职于一家主要开发 Web 应用程序的本地 Microsoft 认证合作伙伴。她今年 34 岁,从事软件工作已有 11 年。这使她成为更有经验的开发人员之一在公司里。除了履行作为高级开发人员的常规职责外,她还经常担任初级开发人员的导师。总的来说,Mary 对她所做的工作很满意,但令她感到沮丧的是,里程碑经常被错过,这迫使她和她的同事长时间工作并在周末工作以赶上最后期限。

Mary Rowan is a professional .NET developer working for a local Certified Microsoft Partner that mainly develops web applications. She’s 34 years old and has been working with software for 11 years. This makes her one of the more experienced developers in the company. In addition to performing her regular duties as a senior developer, she often acts as a mentor for junior developers. In general, Mary is happy about the work that she’s doing, but it frustrates her that milestones are often missed, forcing her and her colleagues to work long hours and weekends to meet deadlines.

她怀疑必须有更有效的方法来构建软件。为了学习效率,她买了很多编程书籍,但很少有时间阅读,因为她的大部分业余时间都花在了丈夫和两个女儿身上。玛丽喜欢去山里远足。她也是一位热心的厨师,她绝对知道如何制作真正的贝恩酱酱。

She suspects that there must be more efficient ways to build software. In an effort to learn about efficiency, she buys a lot of programming books, but she rarely has time to read them, as much of her spare time is spent with her husband and two girls. Mary likes to go hiking in the mountains. She’s also an enthusiastic cook, and she definitely knows how to make a real sauce béarnaise.

Mary 被要求在 ASP.NET Core MVC 和 Entity Framework Core 上创建一个新的电子商务应用程序,并将 SQL Server 作为数据存储。为了最大化模块化,它必须是一个三层应用程序。

Mary has been asked to create a new e-commerce application on ASP.NET Core MVC and Entity Framework Core with SQL Server as the data store. To maximize modularity, it must be a three-layer application.

要实现的第一个功能应该是一个简单的特色产品列表,从数据库表中提取并显示在网页上(示例如图 2.2所示)。并且,如果查看列表的用户是优先客户,则所有产品的价格都应有 5% 的折扣。

The first feature to implement should be a simple list of featured products, pulled from a database table and displayed on a web page (an example is shown in figure 2.2). And, if the user viewing the list is a preferred customer, the price on all products should be discounted by 5%.

02-02.tif

图 2.2 Mary 被要求开发的电子商务 Web 应用程序的屏幕截图。它具有特色产品及其价格的简单列表。

Figure 2.2 Screen capture of the e-commerce web application Mary has been asked to develop. It features a simple list of featured products and their prices.

为了完成她的第一个功能,Mary 必须执行以下操作:

To complete her first feature, Mary will have to implement the following:

  • 数据层— 在数据库中包含一个 Products 表,它表示所有数据库行,以及一个Product类,它表示单个数据库行
  • A data layer — Includes a Products table in the database, which represents all database rows, and a Product class, which represents a single database row
  • 领域层— 包含检索特色产品的逻辑
  • A domain layer — Contains the logic for retrieving the featured products
  • 用户界面层使用 MVC 控制器- 处理传入请求,从域层检索相关数据,并将其发送到 Razor 视图,最终呈现特色产品列表
  • A UI Layer with an MVC controller — Handles incoming requests, retrieves the relevant data from the domain layer, and sends it to the Razor view, which eventually renders the list of featured products

让我们看看 Mary 在实现应用程序的第一个功能时的工作。

Let’s look over Mary’s shoulder as she implements the application’s first feature.

2.1.2 创建数据层

2.1.2 Creating the data layer

因为 Mary 需要从数据库表中提取数据,所以她决定从实现数据层开始。第一步是定义数据库表本身。Mary 使用 SQL Server Management Studio 创建表 2.1中所示的表。

Because Mary will need to pull data from a database table, she has decided to begin by implementing the data layer. The first step is to define the database table itself. Mary uses SQL Server Management Studio to create the table shown in table 2.1.

表 2.1 Mary 创建了包含以下列的 Products 表。
列名数据类型允许空值首要的关键
ID唯一标识符是的
姓名nvarchar(50)
描述nvarchar(最大值)
单价
是精选少量

为了实现数据访问层,Mary 在她的解决方案中添加了一个新库。以下清单显示了她的Product班级.

To implement the data access layer, Mary adds a new library to her solution. The following listing shows her Product class.

清单 2.1 玛丽的Product班级

Listing 2.1 Mary’s Product class

public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal UnitPrice { get; set; }
    public bool IsFeatured { get; set; }
}

Mary 使用 Entity Framework 来满足她的数据访问需求。她将 Microsoft.EntityFrameworkCore.SqlServer NuGet 包的依赖项添加到她的项目中,并实现了一个特定于应用程序的DbContext允许她的应用程序通过CommerceContext类访问 Products 表. 以下清单显示了她的CommerceContext班级。

Mary uses Entity Framework for her data access needs. She adds a dependency to the Microsoft.EntityFrameworkCore.SqlServer NuGet package to her project, and implements an application-specific DbContext class that allows her application to access the Products table via the CommerceContext class. The following listing shows her CommerceContext class.

清单 2.2 Mary 的CommerceContext班级

Listing 2.2 Mary’s CommerceContext class

public class CommerceContext : Microsoft.EntityFrameworkCore.DbContext
{
    public DbSet<Product> Products { get; set; }    ①  

    protected override void OnConfiguring(    ②  
        DbContextOptionsBuilder builder)
    {
        var config = new ConfigurationBuilder()    ③  
            .SetBasePath(    ③  
                Directory.GetCurrentDirectory())    ③  
            .AddJsonFile("appsettings.json")    ③  
            .Build();    ③  

        string connectionString =    ④  
            config.GetConnectionString(    ④  
                "CommerceConnectionString");    ④  
    ④  
        builder.UseSqlServer(connectionString);    ④  
    }
}

因为从配置文件加载连接字符串,所以需要创建该文件。Mary 将一个名为 appsettings.json 的文件添加到她的 Web 项目中,其中包含以下内容:CommerceContext

Because CommerceContext loads a connection string from a configuration file, that file needs to be created. Mary adds a file named appsettings.json to her web project, with the following content:

{
  "ConnectionStrings": {
    "CommerceConnectionString":
      "Server=.;Database=MaryCommerce;Trusted_Connection=True;"
  }
}

CommerceContext并且Product是包含在同一程序集中的公共类型。Mary 知道她稍后需要向她的应用程序添加更多功能,但是实现第一个功能所需的数据访问组件现在已经完成(图 2.3)。

CommerceContext and Product are public types contained within the same assembly. Mary knows that she’ll later need to add more features to her application, but the data access component required to implement the first feature is now completed (figure 2.3).

02-03.eps

图 2.3 Mary 在实现图 2.1中设想的分层架构方面取得了多大进展。

Figure 2.3 How far Mary has come in implementing the layered architecture envisioned in figure 2.1.

现在已经实现了数据访问层,下一个合乎逻辑的步骤是域层。领域也称为领域逻辑层、业务层或业务逻辑层。域逻辑是应用程序需要具有的所有行为,特定于应用程序的构建域。

Now that the data access layer has been implemented, the next logical step is the domain layer. The domain layer is also referred to as the domain logic layer, business layer, or business logic layer. Domain logic is all the behavior that the application needs to have, specific to the domain the application is built for.

2.1.3 创建领域层

2.1.3 Creating the domain layer

除了纯数据报告应用程序之外,始终存在域逻辑。一开始您可能没有意识到,但随着您对领域的了解,其内嵌和隐含的规则和假设将逐渐浮出水面。在没有任何域逻辑的情况下,CommerceContext从技术上讲,可以直接从 UI 层使用所公开的产品列表。

With the exception of pure data-reporting applications, there’s always domain logic. You may not realize it at first, but as you get to know the domain, its embedded and implicit rules and assumptions will gradually emerge. In the absence of any domain logic, the list of products exposed by CommerceContext could technically have been used directly from the UI layer.

Mary 的应用程序要求向优先客户显示产品标价和 5% 的折扣。Mary 还没有弄清楚如何确定首选客户,因此她向她的同事 Jens 寻求建议:

The requirements for Mary’s application state that preferred customers should be shown the product list prices with a 5% discount. Mary has yet to figure out how to identify a preferred customer, so she asks her coworker Jens for advice:

玛丽:我需要实施这个业务逻辑,以便优先客户获得 5% 的折扣。

Mary: I need to implement this business logic so that a preferred customer gets a 5% discount.

延斯:听起来很简单。只需乘以 0.95。

Jens: Sounds easy. Just multiply by .95.

玛丽:谢谢,但这不是我想问你的。我想问你的是,我应该如何识别一个优先客户?

Mary: Thanks, but that’s not what I wanted to ask you about. What I wanted to ask you is, how should I identify a preferred customer?

詹斯:我明白了。这是 Web 应用程序还是桌面应用程序?

Jens: I see. Is this a web application or a desktop application?

玛丽:这是一个网络应用程序。

Mary: It’s a web app.

Jens:好的,那么你可以使用该User属性HttpContext检查当前用户是否在角色中PreferredCustomer

Jens: Okay, then you can use the User property of the HttpContext to check if the current user is in the role PreferredCustomer.

玛丽:慢点,延斯。此代码必须在域层中。这是一个图书馆。没有HttpContext.

Mary: Slow down, Jens. This code must be in the domain layer. It’s a library. There’s no HttpContext.

詹斯:哦。[想了想]我仍然认为你应该使用HttpContextASP.NET 来为用户查找值。然后您可以将该值作为布尔值传递给您的域逻辑。

Jens: Oh. [Thinks for a while] I still think you should use the HttpContext of ASP.NET to look up the value for the user. You can then pass the value to your domain logic as a boolean.

玛丽:我不知道……

Mary: I don’t know...

Jens:这也将确保您有良好的关注点分离,因为您的领域逻辑不必处理安全问题。你知道,单一职责原则!这是做到这一点的敏捷方法!

Jens: That’ll also ensure that you have good Separation of Concerns because your domain logic doesn’t have to deal with security. You know, the Single Responsibility Principle! It’s the Agile way to do it!

玛丽:我想你说得有道理。

Mary: I guess you’ve got a point.

Jens 的建议基于他对 ASP.NET 的技术知识。当讨论把他带离自己的舒适区时,他用流行语的三重组合来压制玛丽。请注意,Jens 并不知道他在说什么:

Jens is basing his advice on his technical knowledge of ASP.NET. As the discussion takes him away from his comfort zone, he steamrolls Mary with a triple combo of buzzwords. Be aware that Jens doesn’t know what he’s talking about:

  • 他滥用了关注点分离的概念. 尽管将安全问题与域逻辑分开很重要,但将其移至表示层无助于分离问题。
  • He misuses the concept of Separation of Concerns. Although it’s important to separate security concerns from the domain logic, moving this to the presentation layer doesn’t help in separating concerns.
  • 他之所以提到敏捷,是因为他最近听到其他人热情地谈论它。
  • He only mentions Agile because he recently heard someone else talk enthusiastically about it.
  • 他完全忽略了单一职责原则的要点. 尽管敏捷方法提供的快速反馈周期可以帮助您相应地改进软件设计,但作为软件设计原则的单一职责原则本身独立于所选的软件开发方法。
  • He completely misses the point of the Single Responsibility Principle. Although the quick feedback cycle that Agile methodologies provide can help you improve your software design accordingly, by itself, the Single Responsibility Principle as a software design principle is independent of the chosen software development methodology.

不幸的是,有了 Jens 糟糕的建议,Mary 创建了一个新的 C# 库项目并添加了一个名为 的类,如清单 2.3所示。为了使该类编译,她必须添加对她的数据访问层的引用,因为该类是在那里定义的。ProductServiceProductServiceCommerceContext

Armed with Jens’ unfortunately poor advice, Mary creates a new C# library project and adds a class called ProductService, shown in listing 2.3. To make the ProductService class compile, she must add a reference to her data access layer, because the CommerceContext class is defined there.

清单 2.3 Mary 的ProductService班级

Listing 2.3 Mary’s ProductService class

public class ProductService
{
    private readonly CommerceContext dbContext;

    public ProductService()
    {
        this.dbContext = new CommerceContext();    ①  
    }

    public IEnumerable<Product> GetFeaturedProducts(
        bool isCustomerPreferred)
    {
        decimal discount =
            isCustomerPreferred ? .95m : 1;

        var featuredProducts =    ②  
            from product in this.dbContext.Products    ②  
            where product.IsFeatured    ②  
            select product;

        return
            from product in    ③  
                featuredProducts.AsEnumerable()    ③  
            select new Product    ③  
            {    ③  
                Id = product.Id,    ③  
                Name = product.Name,    ③  
                Description = product.Description,    ③  
                IsFeatured = product.IsFeatured,    ③  
                UnitPrice =    ③  
                    product.UnitPrice * discount    ③  
            };
    }
}

Mary 很高兴她在ProductService类中封装了数据访问技术 (Entity Framework Core)、配置和域逻辑。她通过传入isCustomerPreferred参数将用户的知识委托给了调用者,她使用此值计算所有产品的折扣。

Mary’s happy that she has encapsulated the data access technology (Entity Framework Core), configuration, and domain logic in the ProductService class. She has delegated the knowledge of the user to the caller by passing in the isCustomerPreferred parameter, and she uses this value to calculate the discount for all the products.

进一步的改进可能包括用可配置的数字替换硬编码的折扣值 (.95),但就目前而言,这种实现就足够了。玛丽快完成了。唯一剩下的就是 UI 层。玛丽决定可以等到明天。图 2.4显示了 Mary 在实现图 2.1中设想的体系结构方面取得了多大进展。

Further refinement could include replacing the hard-coded discount value (.95) with a configurable number, but, for now, this implementation will suffice. Mary’s almost done. The only thing still left is the UI layer. Mary decides that it can wait until tomorrow. Figure 2.4 shows how far Mary has come with implementing the architecture envisioned in figure 2.1.

02-04.eps

图 2.4图 2.3相比,Mary 现在已经实现了数据访问层和领域层。UI层仍然有待实现。

Figure 2.4 Compared to figure 2.3, Mary has now implemented the data access layer and the domain layer. The UI layer still remains to be implemented.

Mary 没有意识到的是,通过让依赖项依赖于数据访问层的类,她将她的领域层与数据访问层紧密耦合。我们将在 2.2 节中解释这有什么问题。ProductServiceCommerceContext

What Mary doesn’t realize is that by letting the ProductService depend on the data access layer’s CommerceContext class, she tightly coupled her domain layer to the data access layer. We’ll explain what’s wrong with that in section 2.2.

2.1.4 创建UI层

2.1.4 Creating the UI layer

第二天,Mary 继续使用电子商务应用程序,将新的 ASP.NET Core MVC 应用程序添加到她的解决方案中。如果您不熟悉 ASP.NET Core MVC 框架,请不要担心。MVC 框架如何运作的复杂细节不是本次讨论的重点。重要的部分是如何使用依赖项,这是一个相对平台中立的主题。

The next day, Mary resumes her work with the e-commerce application, adding a new ASP.NET Core MVC application to her solution. Don’t worry if you aren’t familiar with the ASP.NET Core MVC framework. The intricate details of how the MVC framework operates aren’t the focus of this discussion. The important part is how Dependencies are consumed, and that’s a relatively platform-neutral subject.

下一个清单显示了 Mary 如何实现一个Index方法在她的HomeController课堂上从数据库中提取特色产品并将它们传递给视图。为了使这段代码编译通过,她必须添加对数据访问层和域层的引用。这是因为ProductService类是在领域层定义的,而Product类是在数据访问层定义的。

The next listing shows how Mary implements an Index method on her HomeController class to extract the featured products from the database and pass them to the view. To make this code compile, she must add references to both the data access layer and the domain layer. This is because the ProductService class is defined in the domain layer, but the Product class is defined in the data access layer.

Index默认控制器类上的清单 2.4方法

Listing 2.4 Index method on the default controller class

public ViewResult Index()
{
    bool isPreferredCustomer =    ①  
        this.User.IsInRole("PreferredCustomer");    ①  

    var service = new ProductService();    ②  

    var products = service.GetFeaturedProducts(    ③  
        isPreferredCustomer);    ③  

    this.ViewData["Products"] = products;    ④  

    return this.View();
}

作为 ASP.NET Core MVC 生命周期的一部分,User属性类上的HomeController会自动填充正确的用户对象,因此 Mary 使用它来确定当前用户是否是首选客户。有了这些信息,她就可以调用域逻辑来获取特色产品列表。

As part of the ASP.NET Core MVC lifecycle, the User property on the HomeController class is automatically populated with the correct user object, so Mary uses it to determine if the current user is a preferred customer. Armed with this information, she can invoke the domain logic to get the list of featured products.

在 Mary 的应用程序中,产品列表必须由Index视图呈现。以下清单显示了视图的标记。

In Mary’s application, the list of products must be rendered by the Index view. The following listing shows the markup for the view.

清单 2.5 Index视图标记

Listing 2.5 Index view markup

<h2>Featured Products</h2>
<div>
@{
    var products =    ①  
        (IEnumerable<Product>)this.ViewData["Products"];    ①  

    foreach (Product product in products)    ②  
    {
        <div>@product.Name (@product.UnitPrice.ToString("C"))</div>
    }
}
</div>

ASP.NET Core MVC 允许您编写标准 HTML,其中嵌入了一些命令式代码,以访问由创建视图的控制器创建和分配的对象。在这种情况下,HomeControllerIndex方法将特色产品列表分配给一个名为ProductsMary 在视图中用于呈现产品列表的键。图 2.5显示了 Mary 现在如何实现图 2.1中设想的体系结构。

ASP.NET Core MVC lets you write standard HTML with bits of imperative code embedded to access objects created and assigned by the controller that created the view. In this case, the HomeController’s Index method assigned the list of featured products to a key called Products that Mary uses in the view to render the list of products. Figure 2.5 shows how Mary has now implemented the architecture envisioned in figure 2.1.

02-05.eps

图 2.5 Mary 现在已经在应用程序中实现了所有三个层。

Figure 2.5 Mary has now implemented all three layers in the application.

所有三层都到位后,应用程序理论上应该可以正常工作。但只有运行该应用程序,她才能验证情况是否如此。

With all three layers in place, the applications should theoretically work. But only by running the application can she verify whether that’s the case.

2.2 评估紧耦合应用

2.2 Evaluating the tightly coupled application

Mary 现在已经实现了所有三个层,所以是时候看看应用程序是否正常工作了。她按下F5,出现如图2.2所示的网页。Featured Products 功能现已完成,Mary 充满信心并准备好在应用程序中实施下一个功能。毕竟,她遵循既定的最佳实践并创建了一个三层应用程序……或者她做到了吗?

Mary has now implemented all three layers, so it’s time to see if the application works. She presses F5 and the web page shown in figure 2.2 appears. The Featured Products feature is now done, and Mary feels confident and ready to implement the next feature in the application. After all, she followed established best practices and created a three-layer application ... or did she?

Mary 是否成功开发了合适​​的分层应用程序?不,她没有,尽管她当然是出于好意。她创建了三个 Visual Studio 项目,对应于计划架构中的三个层。对于不经意的观察者来说,这看起来像是梦寐以求的分层架构,但是,正如您将看到的,代码是紧密耦合的。

Did Mary succeed in developing a proper, layered application? No, she didn’t, although she certainly had the best of intentions. She created three Visual Studio projects that correspond to the three layers in the planned architecture. To the casual observer, this looks like the coveted layered architecture, but, as you’ll see, the code is tightly coupled.

Visual Studio 使以这种方式处理解决方案和项目变得简单自然。如果您需要来自不同库的功能,您可以轻松地添加对它的引用并编写代码来创建其他库中定义的类型的新实例。但是,每次添加引用时,您都会承担一个Dependency

Visual Studio makes it easy and natural to work with solutions and projects in this way. If you need functionality from a different library, you can easily add a reference to it and write code that creates new instances of the types defined in the other libraries. Every time you add a reference, though, you take on a Dependency.

2.2.1 评估依赖图

2.2.1 Evaluating the dependency graph

在 Visual Studio 中使用解决方案时,很容易忘记重要的依赖项。这是因为 Visual Studio 将它们与可能指向 .NET 基类库中的程序集的所有其他项目引用一起显示(BCL)。要了解 Mary 的应用程序中的模块如何相互关联,我们可以绘制依赖关系图(见图 2.6)。

When working with solutions in Visual Studio, it’s easy to lose track of the important Dependencies. This is because Visual Studio displays them together with all the other project references that may point to assemblies in the .NET Base Class Library (BCL). To understand how the modules in Mary’s application relate to each other, we can draw a graph of the dependencies (see figure 2.6).

02-06.eps

图 2.6 Mary 的应用程序的依赖关系图,显示了模块如何相互依赖。箭头指向模块的依赖性。

Figure 2.6 The dependency graph for Mary’s application, showing how the modules depend on each other. The arrows point towards a module’s dependency.

从图 2.6中获得的最显着的洞察力是 UI 层同时依赖于域和数据访问层。在某些情况下,UI 似乎可以绕过领域层。这需要进一步调查。

The most remarkable insight to be gained from figure 2.6 is that the UI layer depends on both domain and data access layers. It seems as though the UI could bypass the domain layer in certain cases. This requires further investigation.

2.2.2 评估可组合性

2.2.2 Evaluating composability

构建三层应用程序的一个主要目标是分离关注点。我们希望将我们的域模型与数据访问层和 UI 层分开,以便这些问题都不会污染域模型。在大型应用程序中,能够独立处理应用程序的每个区域至关重要。为了评估 Mary 的实现,我们可以问一个简单的问题:是否可以单独使用每个模块?

A major goal of building a three-layer application is to separate concerns. We’d like to separate our domain model from the data access and UI layers so that none of these concerns pollute the domain model. In large applications, it’s essential to be able to work with each area of the application in isolation. To evaluate Mary’s implementation, we can ask a simple question: Is it possible to use each module in isolation?

理论上,我们应该能够以任何我们喜欢的方式组合模块。我们可能需要编写新模块以新的和意想不到的方式将现有模块绑定在一起,但理想情况下,我们应该能够这样做而不必修改现有模块。我们能否以令人兴奋的新方式使用 Mary 的应用程序中的模块?让我们看看一些可能的情况。

In theory, we should be able to compose modules any way we like. We may need to write new modules to bind existing modules together in new and unanticipated ways, but, ideally, we should be able to do so without having to modify the existing modules. Can we use the modules in Mary’s application in new and exciting ways? Let’s look at some likely scenarios.

构建新的用户界面

Building a new UI

如果 Mary 的应用程序成功,项目利益相关者希望她在 Windows Presentation Foundation (WPF). 在重用域和数据访问层时可以这样做吗?

If Mary’s application becomes a success, the project stakeholders would like her to develop a rich client version in Windows Presentation Foundation (WPF). Is this possible to do while reusing the domain and data access layers?

当我们检查图 2.6中的依赖关系图时,我们可以快速确定没有模块依赖于 Web UI,因此可以删除它并用 WPF UI 替换它。创建基于 WPF 的富客户端是一个新的应用程序,它与原始 Web 应用程序共享其大部分实现。图 2.7说明了 WPF 应用程序如何需要采用与 Web 应用程序相同的依赖项。原始 Web 应用程序可以保持不变。

When we examine the dependency graph in figure 2.6, we can quickly ascertain that no modules are depending on the web UI, so it’s possible to remove it and replace it with a WPF UI. Creating a rich client based on WPF is a new application that shares most of its implementation with the original web application. Figure 2.7 illustrates how a WPF application would need to take the same dependencies as the web application. The original web application can remain unchanged.

02-07.eps

图 2.7 用 WPF UI 替换 Web UI 是可能的,因为没有模块依赖于 Web UI。虚线框表示我们要替换的部分。

Figure 2.7 Replacing a web UI with a WPF UI is possible because no module depends on the web UI. The dashed box signals the part that we want to replace.

Mary 的实现当然可以替换 UI 层。让我们检查另一个有趣的分解。

Replacing the UI layer is certainly possible with Mary’s implementation. Let’s examine another interesting decomposition.

构建新的数据访问层

Building a new data access layer

玛丽的市场分析师发现,为了优化利润,她的应用程序应该作为托管在 Microsoft Azure 上的云应用程序提供. 在蔚蓝,数据可以存储在高度可扩展的 Azure 表存储服务中。这种存储机制基于包含不受约束数据的灵活数据容器。该服务不强制执行特定的数据库模式,也没有参照完整性。

Mary’s market analysts figure out that, to optimize profits, her application should be available as a cloud application hosted on Microsoft Azure. In Azure, data can be stored in the highly scalable Azure Table Storage Service. This storage mechanism is based on flexible data containers that contain unconstrained data. The service enforces no particular database schema, and there’s no referential integrity.

尽管 .NET 上最常见的数据访问技术是基于 ADO.NET 数据服务,用于与表存储服务通信的协议是 HTTP。这种类型的数据库有时称为键值数据库,它与通过 Entity Framework Core 访问的关系数据库不同。

Although the most common data access technology on .NET is based on ADO.NET Data Services, the protocol used to communicate with the Table Storage Service is HTTP. This type of database is sometimes known as a key-value database, and it’s a different beast than a relational database accessed through Entity Framework Core.

要使电子商务应用程序成为云应用程序,必须将数据访问层替换为使用表存储服务的模块。这可能吗?

To enable the e-commerce application as a cloud application, the data access layer must be replaced with a module that uses the Table Storage Service. Is this possible?

图 2.6的依赖图中,我们已经知道 UI 层和领域层都依赖于基于 Entity Framework 的数据访问层。如果我们尝试删除数据访问层,解决方案将不再编译而不重构所有其他项目,因为缺少必需的依赖项。在一个有几十个模块的大型应用程序中,我们也可以尝试删除不编译的模块,看看会剩下什么。对于 Mary 的应用程序,很明显我们必须删除所有模块,不留下任何东西,如图 2.8所示。

From the dependency graph in figure 2.6, we already know that both the UI and domain layers depend on the Entity Framework–based data access layer. If we try to remove the data access layer, the solution will no longer compile without refactoring all other projects because a required Dependency is missing. In a big application with dozens of modules, we could also try to remove the modules that don’t compile to see what would be left. In the case of Mary’s application, it’s evident that we’d have to remove all modules, leaving nothing behind, as figure 2.8 shows.

02-08.eps

图 2.8 替换关系数据访问层的尝试

Figure 2.8 An attempt to replace the relational data access layer

尽管可以开发 Azure 表数据访问层模仿原始数据访问层公开的 API,我们无法在不触及应用程序其他部分的情况下将其应用于应用程序。该应用程序的可组合性远不如项目利益相关者所希望的那样。启用利润最大化的云功能需要对应用程序进行重大重写,因为现有模块都不能重复使用。

Although it would be possible to develop an Azure Table data access layer that mimics the API exposed by the original data access layer, there’s no way we could apply that to the application without touching other parts of the application. The application isn’t nearly as composable as the project stakeholders would have liked. Enabling the profit-maximizing cloud abilities requires a major rewrite of the application because none of the existing modules can be reused.

评估其他组合

Evaluating other combinations

我们可以分析其他模块组合的应用程序,但这将是一个有争议的问题,因为我们已经知道它无法支持一个重要的场景。此外,并非所有组合都有意义。

We could analyze the application for other combinations of modules, but this would be a moot point because we already know that it fails to support an important scenario. Besides, not all combinations make sense.

例如,我们可以询问是否有可能用不同的实现替换域模型。但是,在大多数情况下,这是一个奇怪的问题,因为域模型封装了应用程序的核心。没有领域模型,大多数应用程序就没有存在的理由。

For instance, we could ask whether it would be possible to replace the domain model with a different implementation. But, in most cases, this would be an odd question to ask because the domain model encapsulates the heart of the application. Without the domain model, most applications have no reason to exist.

2.3 缺失可组合性分析

2.3 Analysis of missing composability

为什么 Mary 的实现未能达到预期的可组合性程度?是不是因为UI直接依赖于数据访问层?让我们更详细地研究这种可能性。

Why did Mary’s implementation fail to achieve the desired degree of composability? Is it because the UI has a direct dependency on the data access layer? Let’s examine this possibility in greater detail.

2.3.1 依赖图分析

2.3.1 Dependency graph analysis

为什么UI依赖于数据访问库?罪魁祸首是这个领域模型的方法签名:

Why does the UI depend on the data access library? The culprit is this domain model’s method signature:

02-11_hedgehog.eps

GetFeaturedProducts方法_ProductService班级的返回一系列产品,但Product该类是在数据访问层中定义的。任何使用该GetFeaturedProducts方法的客户端都必须引用数据访问层才能进行编译。可以更改方法的签名以返回域模型中定义的类型。它也会更正确,但它不能解决问题。

The GetFeaturedProducts method of the ProductService class returns a sequence of products, but the Product class is defined in the data access layer. Any client consuming the GetFeaturedProducts method must reference the data access layer to be able to compile. It’s possible to change the signature of the method to return a type defined within the domain model. It’d also be more correct, but it doesn’t solve the problem.

02-09.eps

图 2.9 UI 对数据访问层的依赖被去除的假设情况的依赖图

Figure 2.9 Dependency graph of the hypothetical situation where the UI’s dependency on the data access layer is removed

假设我们打破了 UI 和数据访问库之间的依赖关系. 修改后的依赖关系图现在如图 2.9 所示

Let’s assume that we break the dependency between the UI and data access library. The modified dependency graph would now look like figure 2.9.

这样的更改是否能让 Mary 将关系数据访问层替换为封装对 Azure 表服务的访问的层?不幸的是,没有,因为领域层仍然依赖于数据访问层。反过来,UI 仍然依赖于域模型。如果我们试图移除原始数据访问层,应用程序将一无所有。问题的根本原因在别处。

Would such a change enable Mary to replace the relational data access layer with one that encapsulates access to the Azure Table service? Unfortunately, no, because the domain layer still depends on the data access layer. The UI, in turn, still depends on the domain model. If we try to remove the original data access layer, there’d be nothing left of the application. The root cause of the problem lies somewhere else.

2.3.2 数据访问接口分析

2.3.2 Data access interface analysis

域模型依赖于数据访问层,因为整个数据模型都在那里定义。使用实体框架来实现数据访问层可能是一个合理的决定。但是,从松散耦合的角度来看,直接在域模型中使用它并不是。

The domain model depends on the data access layer because the entire data model is defined there. Using Entity Framework to implement a data access layer may be a reasonable decision. But, from the perspective of loose coupling, consuming it directly in the domain model isn’t.

有问题的代码散布在ProductService课堂上。构造函数创建CommerceContext类的新实例并将其分配给私有成员变量:

The offending code can be found spread out in the ProductService class. The constructor creates a new instance of the CommerceContext class and assigns it to a private member variable:

this.dbContext = new CommerceContext();

ProductService这将类与数据访问层紧密耦合。没有合理的方法可以拦截这段代码并将其替换为其他代码。对数据访问层的引用被硬编码到ProductService类中!

This tightly couples the ProductService class to the data access layer. There’s no reasonable way you can Intercept this piece of code and replace it with something else. The reference to the data access layer is hard-coded into the ProductService class!

GetFeaturedProducts方法的实现用于从数据库CommerceContext中提取Product对象:

The implementation of the GetFeaturedProducts method uses CommerceContext to pull Product objects from the database:

var featuredProducts =
    from product in this.dbContext.Products
    where product.IsFeatured
    select product;

CommerceContext对within的引用GetFeaturedProducts加强了硬编码的依赖性,但此时,损害已经造成。我们需要的是一种没有这种紧密耦合的更好的模块组合方式。如果您回顾第 1 章中讨论的 DI 的好处,您会发现 Mary 的应用程序没有以下内容:

The reference to CommerceContext within GetFeaturedProducts reinforces the hard-coded dependency, but, at this point, the damage is already done. What we need is a better way to compose modules without such tight coupling. If you look back at the benefits of DI as discussed in chapter 1, you’ll see that Mary’s application fails to have the following:

  • 后期绑定 — 由于域层与数据访问层紧密耦合,因此不可能部署同一应用程序的两个版本,其中一个连接到本地 SQL Server 数据库,另一个使用 Azure 表存储托管在 Microsoft Azure 上。换句话说,使用后期绑定加载正确的数据访问层是不可能的。
  • Late binding — Because the domain layer is tightly coupled with the data access layer, it becomes impossible to deploy two versions of the same application, where one connects to a local SQL Server database and the other is hosted on Microsoft Azure using Azure Table Storage. In other words, it’s impossible to load the correct data access layer using late binding.
  • 可扩展性 — 因为应用程序中的所有类都彼此紧密耦合,所以像第 1 章中的安全特性一样插入横切关注点的成本很高。这样做需要更改系统中的许多类。因此,这种紧密耦合的设计不是特别可扩展的。
  • Extensibility — Because all classes in the application are tightly coupled to one another, it becomes costly to plug in Cross-Cutting Concerns like the security feature in chapter 1. Doing so requires many classes in the system to be changed. This tightly coupled design is, therefore, not particularly extensible.
  • 可维护性 — 不仅会添加横切关注点需要在整个应用程序中进行彻底的更改,但是每个新添加的横切关注点都可能会使每个涉及的类更加复杂。每次添加都会使课程更难阅读。这意味着该应用程序不像 Mary 所希望的那样易于维护。
  • Maintainability — Not only would adding Cross-Cutting Concerns require sweeping changes throughout the application, but every newly added Cross-Cutting Concern would likely make each class touched even more complex. Every addition would make a class harder to read. This means that the application isn’t as maintainable as Mary would like.
  • 并行开发 — 如果我们坚持前面应用横切关注点的示例,就很容易理解必须对整个代码库进行全面更改会阻碍与多个开发人员在单个应用程序上并行工作的能力。像我们一样,在过去将您的工作提交到版本控制系统时,您可能已经处理过痛苦的合并冲突。一个设计良好、松散耦合的系统,除其他外,将减少您将拥有的合并冲突的数量。当更多的开发人员开始处理 Mary 的应用程序时,要在不影响彼此的情况下有效地工作会变得越来越难。
  • Parallel development — If we stick with the previous example of applying Cross-Cutting Concerns, it’s quite easy to understand that having to make sweeping changes throughout your code base hinders the ability to work with multiple developers in parallel on a single application. Like us, you’ve likely dealt with painful merge conflicts in the past when committing your work to a version control system. A well-designed, loosely coupled system will, among other things, reduce the amount of merge conflicts that you’ll have. When more developers start working on Mary’s application, it’ll become harder and harder to work effectively without stepping on each other’s toes.
  • 可测试性 — 我们已经确定换出数据访问层目前是不可能的。然而,在没有数据库的情况下测试代码是进行单元测试的先决条件。但即使进行了集成测试,Mary 也可能需要更换部分代码,而当前的设计让这很难做到。因此,Mary 的应用程序是不可测试的。
  • Testability — We already established that swapping out the data access layer is currently impossible. Testing code without a database, however, is a prerequisite for doing unit testing. But even with integration testing, Mary will likely need some parts of the code to be swapped out, and the current design makes this hard. Mary’s application is, therefore, not Testable.

此时,您可能会问自己所需的依赖图应该是什么样子。对于最高程度的重用,最低数量的依赖是可取的。另一方面,如果根本没有依赖关系,应用程序将变得毫无用处。

At this point, you may ask yourself what the desired dependency graph should look like. For the highest degree of reuse, the lowest amount of dependencies is desirable. On the other hand, the application would become rather useless if there were no dependencies at all.

您需要哪些依赖项以及它们应指向的方向取决于需求。但是因为我们已经确定我们无意用完全不同的实现替换域层,所以可以安全地假设其他层可以安全地依赖它。图 2.10对您将在下一章中编写的松耦合应用程序进行了大剧透,但它确实显示了所需的依赖关系图。

Which dependencies you need and in what direction they should point depends on the requirements. But because we’ve already established that we have no intention of replacing the domain layer with a completely different implementation, it’s safe to assume that other layers can safely depend on it. Figure 2.10 contains a big spoiler for the loosely coupled application you’ll write in the next chapter, but it does show the desired dependency graph.

02-10.eps

图 2.10 期望情形的依赖图

Figure 2.10 Dependency graph of the desired situation

该图显示了我们如何反转域和数据访问层之间的依赖关系。我们将在下一章中详细介绍如何执行此操作。

The figure shows how we inverted the dependency between the domain and data access layers. We’ll go into more detail on how to do this in the next chapter.

2.3.3 杂项其他问题

2.3.3 Miscellaneous other issues

我们想指出一些应该解决的 Mary 代码的其他问题。

We’d like to point out a few other issues with Mary’s code that ought to be addressed.

  • 大多数领域模型似乎是在数据访问层中实现的. Product领域层引用数据访问层是一个技术问题,而数据访问层将类定义为类是一个概念问题. 公共Product类属于领域模型。
  • Most of the domain model seems to be implemented in the data access layer. Whereas it’s a technical problem that the domain layer references the data access layer, it’s a conceptual problem that the data access layer defines such a class as the Product class. A public Product class belongs in the domain model.
  • 根据 Jens 的建议,Mary 决定在 UI 中实施确定用户是否为首选客户的代码。但是如何将客户识别为首选客户是一个业务逻辑,因此应该在领域模型中实现。Jens 关于关注点分离的论点单一职责原则不是将代码放在错误位置的借口。在单个库中遵循单一职责原则是完全可能的——这是预期的方法。
  • On Jens’ advice, Mary decided to implement in the UI the code that determines whether a user is a preferred customer. But how a customer is identified as a preferred customer is a piece of business logic, so it should be implemented in the domain model. Jens’ argument about Separation of Concerns and the Single Responsibility Principle is no excuse for putting code in the wrong place. Following the Single Responsibility Principle within a single library is entirely possible — that’s the expected approach.
  • Mary 从CommerceContext类中的配置文件加载连接字符串(如清单 2.2所示)。从其消费者的角度来看,完全隐藏了对该配置值的依赖。正如我们在讨论清单 2.2时提到的,这种隐含包含一个陷阱。

    虽然配置已编译应用程序的能力很重要,但只有完成的应用程序才应该依赖配置文件。对于可重用的库来说,调用者可以强制配置它更加灵活,而不是自己读取配置文件。最后,最终调用者是应用程序的入口点。那时,所有相关的配置数据都可以在启动时直接从配置文件中读取,并根据需要提供给底层库。我们希望CommerceContext需要明确的配置。

  • Mary loaded the connection string from the configuration file from within theCommerceContextclass (shown in listing 2.2). From the perspective of its consumers, the dependency on this configuration value is completely hidden. As we alluded to when discussing listing 2.2, this implicitness contains a trap.

    Although the ability to configure a compiled application is important, only the finished application should rely on configuration files. It’s more flexible for reusable libraries to be imperatively configurable by their callers, instead of reading configuration files themselves. In the end, the ultimate caller is the application’s entry point. At that point, all relevant configuration data can be read from a configuration file directly at startup and fed to the underlying libraries as needed. We want the configuration that CommerceContext requires to be explicit.

  • 视图(如清单 2.5所示)似乎包含太多功能。它执行强制转换和特定的字符串格式化。此类功能应移至基础模型。
  • The view (as shown in listing 2.5) seems to contain too much functionality. It performs casts and specific string formatting. Such functionality should be moved to the underlying model.

2.4 结论

2.4 Conclusion

编写紧密耦合的代码非常容易。即使当 Mary 明确打算编写一个三层应用程序时,它也变成了一个大体上单一的意大利面条代码。5   (当我们谈论分层时,我们称之为烤宽面条。)

It’s surprisingly easy to write tightly coupled code. Even when Mary set out with the express intent of writing a three-layer application, it turned into a largely monolithic piece of Spaghetti Code.5  (When we’re talking about layering, we call this Lasagna.)

编写紧密耦合的代码如此容易的众多原因之一是语言特性和我们的工具已经把我们拉向了那个方向。如果你需要一个对象的新实例,你可以使用new关键字。如果您没有对所需程序集的引用,Visual Studio 可以轻松添加。但是每次你使用new关键字,你引入了一个紧耦合。正如第 1 章所讨论的,并不是所有的紧耦合都是不好的,但是你应该努力防止与易变依赖的紧耦合.

One of the many reasons that it’s so easy to write tightly coupled code is that both the language features and our tools already pull us in that direction. If you need a new instance of an object, you can use the new keyword. If you don’t have a reference to the required assembly, Visual Studio makes it easy to add. But every time you use the new keyword, you introduce a tight coupling. As discussed in chapter 1, not all tight coupling is bad, but you should strive to prevent tight coupling to Volatile Dependencies.

到现在为止,您应该开始理解是什么使紧密耦合的代码如此成问题,但我们还没有向您展示如何解决这些问题。在下一章中,我们将向您展示一种更具组合性的方法来构建具有与 Mary 构建的相同功能的应用程序。我们还将同时解决第 2.3.3 节中讨论的那些其他问题。

By now you should begin to understand what it is that makes tightly coupled code so problematic, but we’ve yet to show you how to fix these problems. In the next chapter, we’ll show you a more composable way of building an application with the same features as the one Mary built. We’ll also address those other issues discussed in section 2.3.3 at the same time.

概括

Summary

  • 复杂的软件必须解决许多不同的问题,例如安全性、诊断、操作、性能和可扩展性。
  • Complex software must address lots of different concerns, such as security, diagnostics, operations, performance, and extensibility.
  • 松散耦合鼓励您孤立地解决所有应用程序问题,但最终您仍必须组合这组复杂的问题。
  • Loose coupling encourages you to address all application concerns in isolation, but ultimately you must still compose this complex set of concerns.
  • 创建紧密耦合的代码很容易。虽然并非所有的紧耦合都是不好的,但与易失性依赖关系的紧耦合是而且应该避免的。
  • It’s easy to create tightly coupled code. Although not all tight coupling is bad, tight coupling to Volatile Dependencies is and should be avoided.
  • 在 Mary 的应用程序中,由于领域层依赖于数据访问层,因此无法用不同的数据访问层替换数据访问层。她的应用程序中的紧耦合导致 Mary 失去了松耦合提供的好处:后期绑定、可扩展性、可维护性、可测试性和并行开发。
  • In Mary’s application, because the domain layer depended on the data access layer, there was no way to replace the data access layer with a different one. The tight coupling in her application caused Mary to lose the benefits that loose coupling provides: late binding, extensibility, maintainability, Testability, and parallel development.
  • 只有完成的应用程序才应该依赖配置文件。应用程序的其他部分不应该从配置文件请求值,而应该由它们的调用者配置。
  • Only the finished application should rely on configuration files. Other parts of the application shouldn’t request values from a configuration file, but should instead be configurable by their callers.
  • 单一职责原则指出每个类应该只有一个改变的理由。
  • The Single Responsibility Principle states that each class should only have one reason to change.
  • 单一职责原则可以从内聚的角度来看。内聚被定义为类或模块的元素的功能相关性。相关性越低,凝聚力越低;凝聚力越低,类违反单一职责原则的可能性就越大。
  • The Single Responsibility Principle can be viewed from the perspective of cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the amount of relatedness, the lower the cohesion; and the lower the cohesion, the greater the chance a class violates the Single Responsibility Principle.

3

编写松散耦合的代码

3

Writing loosely coupled code

在这一章当中

In this chapter

  • 重新设计 Mary 的电子商务应用程序使其松散耦合
  • Redesigning Mary’s e-commerce application to become loosely coupled
  • 分析松散耦合的应用程序
  • Analyzing that loosely coupled application
  • 评估松散耦合的应用程序
  • Evaluating that loosely coupled application

在烤牛排时,一个重要的做法是让肉在切成薄片之前静置。休息时,果汁会重新分配,结果会变得更加多汁。另一方面,如果你切得太早,所有的汁液都会用完,你的肉会变得更干,更不好吃。让这种情况发生将是一种可怕的耻辱,因为您想为您的客人提供您可以提供的最佳品尝体验。尽管了解任何职业的最佳实践都很重要,但了解不良实践并理解为什么这些实践会导致不尽如人意的结果也同样重要。

When it comes to grilling steak, an important practice is to let the meat rest before you cut it into slices. When resting, the juices redistribute, and the results get juicier. If, on the other hand, you cut it too soon, all the juice runs out, and your meat gets drier and less tasty. It’d be a terrible shame to let this happen, because you’d like to give your guests the best tasting experience you can deliver. Although it’s important to know the best practices for any profession, it’s just as important to know the bad practices and to understand why those lead to unsatisfactory results.

了解好的和坏的做法之间的区别对于学习来说是必不可少的。这就是为什么上一章完全致力于紧耦合代码的示例和分析:分析为您提供了原因

Knowing the difference between good and bad practices is essential to learning. This is why the previous chapter was completely devoted to an example and analysis of tightly coupled code: the analysis provided you with the why.

总而言之,松散耦合提供了许多好处——后期绑定、可扩展性、可维护性、可测试性和并行开发。使用紧密耦合,您将失去这些好处。虽然并非所有的紧耦合都是不可取的,但你应努力避免与Volatile Dependencies的紧耦合。此外,您可以使用依赖注入 (DI) 来解决在分析过程中发现的问题。因为 DI 与 Mary 创建她的应用程序的方式截然不同,所以我们不打算修改她现有的代码。相反,我们将从头开始重新创建它。

To summarize, loose coupling provides a number of benefits — late binding, extensibility, maintainability, Testability, and parallel development. With tight coupling, you lose those benefits. Although not all tight coupling is undesirable, you should strive to avoid tight coupling to Volatile Dependencies. Moreover, you can use Dependency Injection (DI) to solve the issues that were discovered during that analysis. Because DI is a radical departure from the way Mary created her application, we’re not going to modify her existing code. Rather, we’re going to re-create it from scratch.

让我们首先简要回顾一下 Mary 的申请。我们还将讨论我们将如何处理重写以及完成后所需的结果。

Let’s start with a short recap of Mary’s application. We’ll also discuss how we’ll approach the rewrite and what the desired result will look like when we’ve finished.

3.1 重建电子商务应用

3.1 Rebuilding the e-commerce application

第2章分析Mary的应用,得出Volatile Dependencies在不同层之间紧密耦合。正如图 3.1中 Mary 的应用程序的依赖关系图所示,领域层和 UI 层都依赖于数据访问层。

The analysis of Mary’s application in chapter 2 concluded that Volatile Dependencies were tightly coupled across the different layers. As the dependency graph of Mary’s application in figure 3.1 shows, both the domain layer and the UI layer depend on the data access layer.

03-01.eps

图 3.1 Mary 的应用程序的依赖图显示了模块如何相互依赖。

Figure 3.1 The dependency graph for Mary’s application shows how the modules depend on each other.

本章的目标是反转领域层和数据访问层之间的依赖关系。这意味着不是域层依赖于数据访问层,数据访问层将依赖领域层,如图3.2所示。

What we’ll aim to achieve in this chapter is to invert the dependency between the domain layer and the data access layer. This means that instead of the domain layer depending on the data access layer, the data access layer will depend on the domain layer, as shown in figure 3.2.

03-02.eps

图 3.2 Mary 应用程序所需反演的依赖图

Figure 3.2 Dependency graph of the desired inversion for Mary’s application

通过创建此反转,我们允许替换数据访问层,而不必完全重写应用程序。(这与 Mary 开发她的应用程序的方式截然不同。)我们还将在此过程中应用几种模式。然后我们将应用构造函数注入,我们在第 1 章讨论过。最后,我们还将使用方法注入Composition Root,我们将在进行时进行讨论。

By creating this inversion, we allow the data access layer to be replaced without having to completely rewrite the application. (This is a radical departure from the way Mary developed her application.) We’ll also apply several patterns along the way. Then we’ll apply Constructor Injection, which we discussed in chapter 1. And finally, we’ll also use Method Injection and Composition Root, which we’ll discuss as we go.

当我们专注于分离应用程序关注点时,这种方法将导致更多的类。Mary 定义了四个类,我们将定义九个类和三个接口。图 3.3更深入地研究了应用程序,并显示了我们将在本章中创建的类和接口。

This approach will lead to quite a few more classes as we focus on separating the application concerns. Where Mary defined four classes, we’ll define nine classes and three interfaces. Figure 3.3 drills a little deeper into the application and shows the classes and interfaces we’ll create throughout this chapter.

03-03.eps

图 3.3 本章末尾的类和接口。接口用虚线标记。

Figure 3.3 The classes and interfaces that we’ll have at the end of this chapter. Interfaces are marked with dashed lines.

图 3.4显示了应用程序中的主要类将如何交互。在本章的最后,我们将再次看一下该图的更详细的版本。

Figure 3.4 shows how the main classes in the application will interact. At the end of this chapter, we’ll take a look at a slightly more detailed version of this diagram again.

03-04.eps

图 3.4 时序图展示了我们在本章构建的电子商务应用程序中 DI 所涉及的元素之间的交互

Figure 3.4 Sequence diagram showing the interaction between elements involved in DI in the e-commerce application that we build in this chapter

当我们编写软件时,我们更喜欢从最重要的地方开始——利益相关者最能看到的部分。在 Mary 的电子商务应用程序中,这通常是 UI。从那里开始,我们开始工作,添加更多功能,直到完成一个功能;然后我们继续下一个。这种由外而内的技术帮助我们专注于所请求的功能,而不会过度设计解决方案。

When we write software, we prefer to start in the most significant place — the part that has most visibility to our stakeholders. As in Mary’s e-commerce application, this is often the UI. From there, we work our way in, adding more functionality until one feature is done; then we move on to the next. This outside-in technique helps us to focus on the requested functionality without overengineering the solution.

在第 2 章中,玛丽使用了相反的方法。她从数据访问层开始,从内到外工作. 如果我们说由内而外的工作是糟糕的,这对我们来说有点苛刻,但正如您稍后将看到的那样,由外而内的方法可以让您更快地反馈您正在构建的内容。因此,我们将以相反的顺序构建应用程序,从 UI 层开始,继续构建域层,最后构建数据访问层。

In chapter 2, Mary used the opposite approach. She started with the data access layer and worked her way out, working inside-out. It would be harsh for us to say that working inside-out is bad, but as you’ll see later, the outside-in approach gives you quicker feedback on what you’re building. We’ll therefore build the application in the opposite order, starting with the UI layer, continuing with the domain layer, and then building the data access layer last.

因为我们实践测试驱动开发(测试驱动开发),一旦我们的由外向内方法提示我们创建一个新类,我们就开始编写单元测试。尽管我们编写了单元测试来创建这个示例,但 TDD 并不是实现和使用 DI 所必需的,因此我们不会在本书中展示这些测试。如果您有兴趣,本书附带的源代码包括测试。让我们直接进入我们的项目并从 UI 开始。

Because we practice Test-Driven Development (TDD), we start by writing unit tests as soon as our outside-in approach prompts us to create a new class. Although we wrote unit tests to create this example, TDD isn’t required to implement and use DI, so we’re not going to show these tests in the book. If you’re interested, the source code that accompanies this book includes the tests. Let’s dive right into our project and begin with the UI.

3.1.1 构建更易于维护的 UI

3.1.1 Building a more maintainable UI

Mary 对特色产品列表的规范是编写一个应用程序,从数据库中提取这些项目并将它们显示在列表中(再次显示在图 3.5中)。因为我们知道项目利益相关者主要对视觉结果感兴趣,所以 UI 似乎是一个很好的起点。

Mary’s specification for the list of featured products was to write an application that extracts those items from the database and displays them in a list (shown again in figure 3.5). Because we know that the project stakeholders will mainly be interested in the visual result, the UI seems like a good place to start.

03-05.tif

图 3.5 电子商务 Web 应用程序的屏幕截图

Figure 3.5 Screen capture of the e-commerce web application

打开 Visual Studio 后要做的第一件事是向解决方案中添加一个新的 ASP.NET Core MVC 应用程序。因为特色产品列表需要放在首页,所以您首先要修改 Index.cshtml 文件以包含以下清单中显示的标记。2个 

The first thing you do after opening Visual Studio is add a new ASP.NET Core MVC application to the solution. Because the list of featured products needs to go on the front page, you start by modifying the Index.cshtml file to include the markup shown in the following listing.2 

清单 3.1 Index.cshtml 视图标记

Listing 3.1 Index.cshtml view markup

@model FeaturedProductsViewModel

<h2>Featured Products</h2>
<div>
    @foreach (ProductViewModel product in this.Model.Products)
    {
        <div>@product.SummaryText</div>
    }
</div>

请注意与 Mary 的原始标记相比,清单 3.1更加清晰。

Notice how much cleaner listing 3.1 is compared to Mary’s original markup.

清单 3.2 MaryIndex在第 2 章中的原始视图标记

Listing 3.2 Mary’s original Index view markup from chapter 2

<h2>Featured Products</h2>
<div>
@{
    var products = (IEnumerable<Product>)this.ViewData["Products"];

    foreach (Product product in products)
    {
        <div>@product.Name (@product.UnitPrice.ToString("C"))</div>
    }
}
</div>

第一个改进是在迭代成为可能之前,您不再将字典项转换为产品序列。您可以使用 MVC 的特殊@model指令轻松完成此操作. 这意味着该Model物业页面的FeaturedProductsViewModel类型. 使用该@model指令,MVC 将确保将从控制器返回的值强制转换为该FeaturedProductsViewModel类型。SummaryText其次,直接从属性中拉取整个产品显示字符串ProductViewModel

The first improvement is that you no longer cast a dictionary item to a sequence of products before iteration is possible. You accomplished this easily by using MVC’s special @model directive. This means that the Model property of the page is of the FeaturedProductsViewModel type. Using the @model directive, MVC will ensure that the value returned from the controller will be cast to the FeaturedProductsViewModel type. Secondly, the entire product display string is pulled directly from the SummaryText property of ProductViewModel.

这两项改进都与封装视图行为的视图特定模型的引入有关。这些模型是普通的旧 CLR 对象(POCO). 3  以下列表提供了它们结构的概要。

Both improvements are related to the introduction of view-specific models that encapsulate the behavior of the view. These models are Plain Old CLR Objects (POCO).3  The following listing provides an outline of their structure.

清单 3.3 和类FeaturedProductsViewModelProductViewModel

Listing 3.3 FeaturedProductsViewModel and ProductViewModel classes

public class FeaturedProductsViewModel
{
    public FeaturedProductsViewModel(
        IEnumerable<ProductViewModel> products)
    {
        this.Products = products;
    }

    public IEnumerable<ProductViewModel> Products    ①  
        { get; }
}

public class ProductViewModel
{
    private static CultureInfo PriceCulture = new CultureInfo("en-US");

    public ProductViewModel(string name, decimal unitPrice)
    {
        this.SummaryText = string.Format(PriceCulture,
            "{0} ({1:C})", name, unitPrice);
    }

    public string SummaryText { get; }    ②  
}

视图模型的使用简化了视图,这很好,因为视图更难测试。它还使 UI 设计人员更容易处理应用程序。

The use of view models simplifies the view, which is good because views are harder to test. It also makes it easier for a UI designer to work on the application.

HomeController必须返回一个带有 的实例的视图,清单 3.1FeaturedProductsViewModel中的代码才能工作。作为第一步,这可以在内部像这样实现:HomeController

HomeController must return a view with an instance of FeaturedProductsViewModel for the code in listing 3.1 to work. As a first step, this can be implemented inside HomeController like this:

public ViewResult Index()
{
    var vm = new FeaturedProductsViewModel(new[]    ①  
    {    ①  
        new ProductViewModel("Chocolate", 34.95m),    ①  
        new ProductViewModel("Asparagus", 39.80m)    ①  
    });    ①  

    return this.View(vm);    ②  
}

我们在方法中硬编码了打折产品列表Index。这不是期望的最终结果,但它使 Web 应用程序能够无错误地执行,并允许我们向利益相关者展示一个不完整但正在运行的应用程序示例(存根),供他们评论。

We hard-coded the list of discounted products inside the Index method. This isn’t the desired end result, but it enables the web application to execute without error and allows us to show the stakeholders an incomplete, but running example of the application (a stub) for them to comment on.

03-06.tif

图 3.6 存根电子商务 Web 应用程序的屏幕截图。这里的产品列表是硬编码的。

Figure 3.6 Screen capture of the stubbed e-commerce web application. Here the product list is hard-coded.

在这个阶段,只实现了 UI 层的存根;域层和数据访问层的完整实现仍然存在。从 UI 开始的一个优势是我们已经拥有可以运行和测试的软件。将此与玛丽在可比阶段的进步进行对比。直到很晚的时候,Mary 才到达可以运行该应用程序的地步。图 3.6显示了存根的 Web 应用程序。

At this stage, only a stub of the UI layer has been implemented; a full implementation of the domain layer and data access layer still remains. One advantage of starting with the UI is that we already have software we can run and test. Contrast this with Mary’s progress at a comparable stage. Only at a much later stage does Mary arrive at a point where she can run the application. Figure 3.6 shows the stubbed web application.

为了HomeController履行其义务并做任何感兴趣的事情,它从领域层请求特色产品列表。这些产品需要应用折扣。在第 2 章中,Mary 在她的ProductService课程中包含了这个逻辑,我们也将这样做。

For our HomeController to fulfill its obligations, and to do anything of interest, it requests a list of featured products from the domain layer. These products need to have discounts applied. In chapter 2, Mary wrapped this logic in her ProductService class, and we’ll do that too.

on的Index方法HomeController应该使用ProductService实例来检索特色产品列表,将它们转换为ProductViewModel实例,然后将它们添加到FeaturedProductsViewModel. HomeController然而,从 的角度来看,它ProductService是一个Volatile Dependency 因为它是一个尚不存在且仍在开发中的 Dependency。如果我们以后要HomeController隔离测试,ProductService并行开发,或者替换或者拦截它,就需要引入一个Seam

The Index method on HomeController should use the ProductService instance to retrieve the list of featured products, convert those to ProductViewModel instances, and then add those to FeaturedProductsViewModel. From the perspective of HomeController, however, ProductService is a Volatile Dependency because it’s a Dependency that doesn’t yet exist and is still in development. If we want to test HomeController in isolation, develop ProductService in parallel, or replace or Intercept it in the future, we need to introduce a Seam.

回想一下对 Mary 的实现的分析,依赖于易失性依赖是一个大罪。一旦这样做,您就会与刚刚使用的类型紧密耦合。为了避免这种紧密耦合,我们将引入一个接口并使用一种称为构造函数注入的技术;实例是如何创建的,由谁创建的,与HomeController.

Recall from the analysis of Mary’s implementation that depending on Volatile Dependencies is a cardinal sin. As soon as you do that, you’re tightly coupled with the type just used. To avoid this tight coupling, we’ll introduce an interface and use a technique called Constructor Injection; how the instance is created, and by whom, is of no concern to HomeController.

清单 3.4 HomeController

Listing 3.4 HomeController class

public class HomeController : Controller
{
    private readonly IProductService productService;

    public HomeController(
        IProductService productService)    ①  
    {
        if (productService == null)    ②  
            throw new ArgumentNullException(
                "productService");

        this.productService = productService;    ③  
    }

    public ViewResult Index()
    {
        IEnumerable<DiscountedProduct> products =
            this.productService.GetFeaturedProducts();    ④  

        var vm = new FeaturedProductsViewModel(    ⑤  
            from product in products
            select new ProductViewModel(product));    ⑥  

        return this.View(vm);
    }
}

正如我们在第 1 章中所述,构造函数注入是通过将所需依赖项指定为类构造函数的参数来静态定义所需依赖项列表的行为。这正是HomeController所做的。在其公共构造函数中,它定义了正确运行所需的依赖项。

As we stated in chapter 1, Constructor Injection is the act of statically defining the list of required Dependencies by specifying them as parameters to the class’s constructor. This is exactly what HomeController does. In its public constructor, it defines what Dependencies it requires for it to function correctly.

第一次听说构造函数注入,我们很难理解真正的好处。它不会将控制依赖关系的负担推给其他一些类吗?是的,确实如此——这就是重点。在n层应用程序中,您可以将该负担一直推到应用程序的顶部,进入组合根

The first time we heard about Constructor Injection, we had a hard time understanding the real benefit. Doesn’t it push the burden of controlling the Dependency onto some other class? Yes, it does — and that’s the whole point. In an n-layer application, you can push that burden all the way to the top of the application into a Composition Root.

因为我们添加了一个带有参数的构造函数,所以如果没有DependencyHomeController就不可能创建一个,这正是我们这样做的原因。但这确实意味着应用程序的主屏幕已损坏,因为 MVC 不知道我们必须如何创建 — 除非您以其他方式指示 MVC。HomeControllerHomeController

Because we added a constructor with an argument to HomeController, it’ll be impossible to create a HomeController without that Dependency, and that’s exactly why we did that. But that does mean that the application’s home screen is broken, because MVC has no idea how our HomeController must be created — unless you instruct MVC otherwise.

事实上,创建HomeController不是 UI 层的关注点;这是Composition Root的责任。5  至此,我们认为UI层已经完成,后面我们会回过头来创建HomeController图 3.7显示了实现图 3.2中设想的体系结构的当前状态。

In fact, the creation of HomeController isn’t a concern of the UI layer; it’s the responsibility of the Composition Root.5  Because of this, we consider the UI layer completed, and we’ll come back to the creation of HomeController later on. Figure 3.7 shows the current state of implementing the architecture envisioned in figure 3.2.

03-07.eps

图3.7 这个阶段只实现了UI层;域和数据访问层尚未得到解决。

Figure 3.7 At this stage, only the UI layer has been implemented; the domain and data access layers have yet to be addressed.

这将我们带到重新创建电子商务应用程序的下一阶段,即领域模型。

This leads us to the next stage in the re-creation of our e-commerce application, the domain model.

3.1.2 构建独立领域模型

3.1.2 Building an independent domain model

域模型是我们添加到解决方案中的普通 C# 库。该库将包含 POCO 和接口。POCO 将对域建模,而接口提供抽象,将作为我们进入域模型的主要外部入口点。他们将提供契约,域模型通过契约与即将到来的数据访问层进行交互。

The domain model is a plain, vanilla C# library that we add to the solution. This library will contain POCOs and interfaces. The POCOs will model the domain while the interfaces provide Abstractions that will serve as our main external entry points into the domain model. They’ll provide the contract through which the domain model interacts with the forthcoming data access layer.

HomeController一节中交付的还没有编译,因为我们还没有定义IProductService 抽象。在本节中,我们将向电子商务应用程序添加一个新的域层项目,并从 MVC 项目中引用域层项目,就像 Mary 所做的那样。结果没问题,但我们会推迟到 3.2 节再进行依赖图分析,以便我们可以为您提供全貌。以下清单显示了IProductService 抽象

The HomeController delivered in the previous section doesn’t compile yet because we haven’t defined the IProductService Abstraction. In this section, we’ll add a new domain layer project to the e-commerce application and a reference to the domain layer project from the MVC project, like Mary did. That will turn out OK, but we’ll postpone doing a dependency graph analysis until section 3.2 so that we can provide you with the full picture. The following listing shows the IProductService Abstraction.

清单 3.5 IProductService界面

Listing 3.5 IProductService interface

public interface IProductService
{
    IEnumerable<DiscountedProduct> GetFeaturedProducts();
}

IProductService代表我们当前领域层的核心,因为它连接了 UI 层和数据访问层。它是将我们的初始应用程序绑定在一起的粘合剂。

IProductService represents the heart of our current domain layer in that it bridges the UI layer with the data access layer. It’s the glue that binds our initial application together.

IProductService 抽象的唯一成员是GetFeaturedProducts方法. DiscountedProduct它返回实例的集合。每个DiscountedProduct包含一个Name和一个UnitPrice。这是一个简单的 POCO 类,如下一个清单所示,这个定义足以让我们编译我们的 Visual Studio 解决方案。

The sole member of the IProductService Abstraction is the GetFeaturedProducts method. It returns a collection of DiscountedProduct instances. Each DiscountedProduct contains a Name and a UnitPrice. It’s a simple POCO class, as can be seen in the next listing, and this definition gives us enough to compile our Visual Studio solution.

清单 3.6 DiscountedProduct POCO 类

Listing 3.6 DiscountedProduct POCO class

public class DiscountedProduct
{
    public DiscountedProduct(string name, decimal unitPrice)
    {
        if (name == null) throw new ArgumentNullException("name");

        this.Name = name;
        this.UnitPrice = unitPrice;
    }

    public string Name { get; }
    public decimal UnitPrice { get; }
}

针对接口而非具体类编程的原则是 DI 的基石。正是这一原则让您可以用一个具体实现替换另一个。在继续之前,我们应该花点时间了解一下接口在本次讨论中的作用。

The principle of programming to interfaces instead of concrete classes is a cornerstone of DI. It’s this principle that lets you replace one concrete implementation with another. Before continuing, we should take a quick moment to recognize the role of interfaces in this discussion.

接下来我们将编写我们的ProductService实现。此类的GetFeaturedProducts方法ProductService应使用IProductRepository实例来检索特色产品列表、应用任何折扣并返回DiscountedProduct实例列表。

Next we’ll write our ProductService implementation. The GetFeaturedProducts method of this ProductService class should use an IProductRepository instance to retrieve the list of featured products, apply any discounts, and return a list of DiscountedProduct instances.

存储库模式提供了对数据访问的通用抽象,因此我们将IProductRepository在域模型库中定义一个抽象。6个 

A common abstraction over data access is provided by the Repository pattern, so we’ll define an IProductRepository abstraction in the domain model library.6 

清单 3.7 IProductRepository

Listing 3.7 IProductRepository

public interface IProductRepository
{
    IEnumerable<Product> GetFeaturedProducts();
}

IProductRepository是数据访问层的接口,从持久性存储返回“原始”实体。相比之下,IProductService应用业务逻辑,例如本例中的折扣,并将实体转换为范围更窄的对象。一个成熟的存储库将有更多的方法来查找和修改产品,但是,遵循由外而内的原则,我们只定义手头任务所需的类和成员。向代码添加功能比删除任何东西都容易。

IProductRepository is the interface to the data access layer, returning “raw” Entities from the persistence store. By contrast, IProductService applies business logic, such as the discount in this case, and converts the Entities to a narrower-focused object. A full-blown Repository would have more methods to find and modify products, but, following the outside-in principle, we only define the classes and members needed for the task at hand. It’s easier to add functionality to code than it is to remove anything.

因为我们的目标是反转领域层和数据访问层之间的依赖关系,IProductRepository所以定义在领域层。在下一节中,我们将创建一个实现作为数据访问层的一部分。这允许我们的依赖指向域层。IProductRepository

Because our goal is to invert the dependency between the domain layer and the data access layer, IProductRepository is defined in the domain layer. In the next section, we’ll create an implementation of IProductRepository as part of the data access layer. This allows our dependency to point at the domain layer.

Product班级_也是用最少的成员实现的,如以下清单所示。

The Product class is also implemented with the bare minimum of members, as shown in the following listing.

清单 3.8 Product实体

Listing 3.8 ProductEntity

public class Product
{
    public string Name { get; set; }    ①  
    public decimal UnitPrice { get; set; }    ①  
    public bool IsFeatured { get; set; }    ①  

    public DiscountedProduct ApplyDiscountFor(
        IUserContext user)    ②  
    {
        bool preferred =    ③  
            user.IsInRole(Role.PreferredCustomer);    ③  
    ③  
        decimal discount = preferred ? .95m : 1.00m;    ③  
    ③  
        return new DiscountedProduct(    ③  
            name: this.Name,    ③  
            unitPrice: this.UnitPrice * discount);    ③  
    }
}

图 3.8说明了ProductService及其Dependencies之间的关系。

Figure 3.8 illustrates the relationship between ProductService and its Dependencies.

03-08.eps

图 3.8 ProductService及其依赖关系

Figure 3.8 ProductService and its Dependencies

GetFeaturedProducts方法_类的ProductService应该使用一个IProductRepository实例来检索特色产品列表,应用任何折扣,并返回一个DiscountedProduct实例列表。ProductService班级_对应于 Mary 的同名类,但现在是纯域模型类,因为它没有对数据访问层的硬编码引用。对于我们的HomeController,我们将再次使用构造函数注入放弃对其易失性依赖项的控制,如下所示。

The GetFeaturedProducts method of the ProductService class should use an IProductRepository instance to retrieve the list of featured products, apply any discounts, and return a list of DiscountedProduct instances. The ProductService class corresponds to Mary’s class of the same name, but is now a pure domain model class because it doesn’t have a hard-coded reference to the data access layer. As with our HomeController, we’re again going to relinquish control of its Volatile Dependencies using Constructor Injection, as shown next.

ProductService带有构造函数注入的清单 3.9

Listing 3.9 ProductService with Constructor Injection

public class ProductService : IProductService
{
    private readonly IProductRepository repository;
    private readonly IUserContext userContext;

    public ProductService(
        IProductRepository repository,    ①  
        IUserContext userContext)    ①  
    {
        if (repository == null)
            throw new ArgumentNullException("repository");
        if (userContext == null)
            throw new ArgumentNullException("userContext");

        this.repository = repository;
        this.userContext = userContext;
    }

    public IEnumerable<DiscountedProduct> GetFeaturedProducts()
    {
        return
            from product in this.repository    ②  
                .GetFeaturedProducts()    ②  
            select product    ②  
                .ApplyDiscountFor(this.userContext);    ③  
    }
}

除了IProductRepositoryProductService构造函数还需要一个实例IUserContext

Besides an IProductRepository, the ProductService constructor requires an instance of IUserContext:

public interface IUserContext
{
    bool IsInRole(Role role);
}

public enum Role { PreferredCustomer }

这是与 Mary 的实现的另一个不同之处,后者仅将布尔值作为GetFeaturedProducts方法的参数,指示用户是否是首选客户。因为决定用户是否是首选客户是域层的一部分,所以将其显式建模为Dependency更为正确。除此之外,有关代表其运行请求的用户的信息是上下文相关的。我们不希望每个控制器都负责收集这些信息。这将是重复的并且容易出错,并且可能导致意外的安全漏洞。

This is another departure from Mary’s implementation, which only took a boolean value as argument to the GetFeaturedProducts method, indicating whether the user is a preferred customer. Because deciding whether a user is a preferred customer is a piece of the domain layer, it’s more correct to explicitly model this as a Dependency. Besides that, information about the user on whose behalf the request is running is contextual. We don’t want every controller to be responsible for gathering this information. That would be repetitive and error prone, and might lead to accidental security bugs.

我们不是让 UI 层向领域层提供此信息,而是让此信息的检索成为ProductService. 该IUserContext接口允许ProductService检索有关当前用户的信息,而无需HomeController提供此信息。HomeController不需要知道哪些角色被授权获得折扣价,也不容易无意中通过传递而不是HomeController启用折扣。这降低了 UI 层的整体复杂性。truefalse

Instead of letting the UI layer provide this information to the domain layer, we allow the retrieval of this information to become an implementation detail of ProductService. The IUserContext interface allows ProductService to retrieve information about the current user without HomeController needing to provide this. HomeController doesn’t need to know which role(s) are authorized for a discount price, nor is it easy for HomeController to inadvertently enable the discount by passing, for example, true instead of false. This reduces the overall complexity of the UI layer.

虽然 .NET 基类库(BCL)包括一个IPrincipal接口,它代表了对应用程序用户进行建模的标准方法,该接口本质上是通用的,并不是为我们的应用程序的特殊需求量身定制的。相反,我们让应用程序定义抽象

Although the .NET Base Class Library (BCL) includes an IPrincipal interface, which represents a standard way of modeling application users, that interface is generic in nature and isn’t tailored for our application’s special needs. Instead, we let the application define the Abstraction.

ProductService.GetFeaturedProducts方法将IUserContext 依赖项传递给该Product.ApplyDiscountFor方法。这种技术被称为方法注入方法注入在短命的情况下特别有用Entities这样的对象(例如我们的例子中的Product Entity)需要Dependencies。尽管细节有所不同,但主要技术保持不变。我们将在第 4 章更详细地讨论这种模式。在这个阶段,应用程序根本不起作用。那是因为仍然存在三个问题:

The ProductService.GetFeaturedProducts method passes the IUserContext Dependency on to the Product.ApplyDiscountFor method. This technique is known as Method Injection. Method Injection is particularly useful in cases where short-lived objects like Entities (such as the Product Entity, in our case) need Dependencies. Although the details vary, the main technique remains the same. We’ll discuss this pattern in more detail in chapter 4. At this stage, the application doesn’t work at all. That’s because three problems remain:

  • 没有具体的实现IProductRepository。这很容易解决。在下一节中,我们将实现一个从数据库中读取特色产品的实体。SqlProductRepository
  • There’s no concrete implementation of IProductRepository. This is easily solved. In the next section, we’ll implement a concrete SqlProductRepository that reads the featured products from the database.
  • 没有具体的实现IUserContext。我们也将在下一节中对此进行介绍。
  • There’s no concrete implementation ofIUserContext. We’ll take a look at this in the next section too.
  • MVC 框架不知道要使用哪种具体类型。IProductService这是因为我们在 的构造函数中引入了一个类型的抽象参数HomeController。这个问题可以通过多种方式解决,但我们更喜欢开发一个自定义的Microsoft.AspNetCore.Mvc.Controllers.IControllerActivator. 这是如何完成的超出了本章的范围,但这是我们将在第 7 章中讨论的主题。只要说这个自定义工厂将创建一个具体的实例ProductService并将其提供给 的构造函数就足够了HomeController
  • The MVC framework doesn’t know which concrete type to use. This is because we introduced an abstract parameter of type IProductService to the constructor of HomeController. This issue can be solved in various ways, but our preference is to develop a custom Microsoft.AspNetCore.Mvc.Controllers.IControllerActivator. How this is done is outside the scope of this chapter, but it’s a subject that we’ll discuss in chapter 7. Suffice it to say that this custom factory will create an instance of the concrete ProductService and supply it to the constructor of HomeController.

在域层中,我们仅使用域层中定义的类型和.NET BCL 的稳定依赖项。领域层的概念被实现为 POCO。在此阶段,只表示一个概念,即Product. 领域层必须能够与外界(如数据库)进行通信。这种需求被建模为抽象(例如存储库),我们必须在领域层变得有用之前用具体的实现替换它。图 3.9显示了实现图 3.2中设想的体系结构的当前状态。

In the domain layer, we work only with types defined within the domain layer and Stable Dependencies of the .NET BCL. The concepts of the domain layer are implemented as POCOs. At this stage, there’s only a single concept represented, namely, a Product. The domain layer must be able to communicate with the outside world (such as databases). This need is modeled as Abstractions (such as Repositories) that we must replace with concrete implementations before the domain layer becomes useful. Figure 3.9 shows the current state of implementing the architecture envisioned in figure 3.2.

03-09.eps

图 3.9 UI 和域层现在都已就位,而数据访问层仍有待实现。

Figure 3.9 The UI and domain layer are now both in place, whereas the data access layer remains to be implemented.

我们成功地编译了我们的领域模型。这意味着我们创建了一个独立于数据访问层的域模型,我们仍然需要创建它。但在我们开始之前,有几点我们想更详细地解释一下。

We succeeded in making our domain model compile. This means that we created a domain model that’s independent of the data access layer, which we still need to create. But before we get to that, there are a few points we’d like to explain in more detail.

依赖倒置原则

Dependency Inversion Principle

我们试图用 DI 完成的大部分工作都与依赖倒置原则有关。8  该原则指出,我们应用程序中的高层模块不应依赖于低层模块;相反,两个级别的模块都应该依赖于Abstractions

Much of what we’re trying to accomplish with DI is related to the Dependency Inversion Principle.8  This principle states that higher-level modules in our applications shouldn’t depend on lower-level modules; instead, modules of both levels should depend on Abstractions.

03-10.eps

图 3.10两个类都没有依赖于抽象, 而是ProductService依赖于抽象SqlProductRepository

Figure 3.10 Instead of ProductService depending on SqlProductRepository, both classes depend on an Abstraction.

这正是我们在定义IProductRepository. 该ProductService组件是较高级别域层模块的一部分,而IProductRepository实现(我们称之为)是较低级别数据访问模块的一部分。我们不是让我们依赖,而是让两者都依赖抽象。实现抽象,同时使用它。图 3.10说明了这一点。SqlProductRepositoryProductServiceSqlProductRepositoryProductServiceSqlProductRepositoryIProductRepositorySqlProductRepositoryProductService

This is exactly what we did when we defined our IProductRepository. The ProductService component is part of the higher-level domain layer module, whereas the IProductRepository implementation — let’s call it SqlProductRepository — is part of the lower-level data access module. Instead of letting our ProductService depend on SqlProductRepository, we let both ProductService and SqlProductRepository depend on the IProductRepository Abstraction. SqlProductRepository implements the Abstraction, while ProductService uses it. Figure 3.10 illustrates this.

Dependency Inversion Principle和 DI 的关系是,Dependency Inversion Principle规定了我们想要完成什么,DI 说明了我们想要如何完成。该原则并未描述消费者应如何获取其Dependencies。然而,许多开发人员并没有意识到依赖倒置原则的另一个有趣的部分。

The relationship between the Dependency Inversion Principle and DI is that the Dependency Inversion Principle prescribes what we would like to accomplish, and DI states how we would like to accomplish it. The principle doesn’t describe how a consumer should get ahold of its Dependencies. Many developers, however, aren’t aware of another interesting part of the Dependency Inversion Principle.

该原则不仅规定了松散耦合,还规定抽象应该由使用抽象的模块拥有。在这种情况下,“拥有”意味着消费模块可以控制抽象的形状,并且它与该模块一起分发,而不是与实现它的模块一起分发。消费模块应该能够以最有利于自身的方式定义抽象

Not only does the principle prescribe loose coupling, it states that Abstractions should be owned by the module using the Abstraction. In this context, “owned” means that the consuming module has control over the shape of the Abstraction, and it’s distributed with that module, rather than with the module that implements it. The consuming module should be able to define the Abstraction in a way that benefits itself the most.

您已经看到我们这样做了两次:都是这样定义的。它们的设计方式最适合领域层,尽管它们的实现分别由 UI 和数据访问层负责,如图 3.11所示。IUserContextIProductRepository

You already saw us do this twice: both IUserContext and IProductRepository are defined this way. They’re designed in a way that works best for the domain layer, even though their implementations are the responsibility of the UI and data access layers, respectively, as shown in figure 3.11.

让更高级别的模块或层定义自己的抽象不仅可以防止它必须依赖于较低级别的模块,还可以简化更高级别的模块,因为抽象是根据其特定需求量身定制的。这将我们带回到 BCL 的IPrincipal界面.

Letting a higher-level module or layer define its own Abstractions not only prevents it from having to take a dependency on a lower-level module, it allows the higher-level module to be simplified, because the Abstractions are tailored for its specific needs. This brings us back to the BCL’s IPrincipal interface.

正如我们所描述的,IPrincipal本质上是通用的。相反,依赖倒置原则指导我们定义为应用程序的特殊需求量身定制的抽象。这就是为什么我们定义自己的IUserContext Abstraction而不是让领域层依赖于IPrincipal. 然而,这确实意味着我们必须创建一个适配器实现,允许将调用从这个特定于应用程序的IUserContext 抽象转换为对应用程序框架的调用。

As we described, IPrincipal is generic in nature. The Dependency Inversion Principle instead guides us towards defining Abstractions tailored for our application’s special needs. That’s why we define our own IUserContext Abstraction instead of letting the domain layer depend on IPrincipal. This does mean, however, that we have to create an Adapter implementation that allows translating calls from this application-specific IUserContext Abstraction to calls to the application framework.

03-11.eps

图 3.11IUserContext和 都是IProductRepository领域层的一部分,因为ProductService“拥有”它们。

Figure 3.11 Both IUserContext and IProductRepository are part of the domain layer, because ProductService “owns” them.

如果依赖倒置原则规定抽象应该与它们拥有的模块一起分发,那么领域层IProductService接口是否违反了这个原则?毕竟,IProductService是由UI层消费的,而是由领域层实现的,如图3.12所示。答案是肯定的,这确实违反了依赖倒置原则

If the Dependency Inversion Principle dictates that Abstractions should be distributed with their owning modules, doesn’t the domain layer IProductService interface violate this principle? After all, IProductService is consumed by the UI layer, but implemented by the domain layer, as figure 3.12 shows. The answer is yes, this does violate the Dependency Inversion Principle.

03-12.eps

图 3.12 通过制作IProductService领域层的一部分,我们违反了依赖倒置原则

Figure 3.12 By making IProductService part of the domain layer, we violate the Dependency Inversion Principle.

如果我们热衷于解决这个违规问题,我们应该IProductService离开领域层。然而IProductService,进入 UI 层会使我们的领域层依赖于该层。因为领域层是应用程序的核心部分,我们不希望它依赖于任何其他东西。此外,这种依赖性会使以后无法替换 UI。

If we were keen on fixing this violation, we should move IProductService out of the domain layer. Moving IProductService into the UI layer, however, would make our domain layer dependent on that layer. Because the domain layer is the central part of the application, we don’t want it to depend on anything else. Besides, this dependency would make it impossible to replace the UI later on.

这意味着要解决违规问题,我们的解决方案中需要另外两个额外的项目——一个用于没有Composition Root的独立 UI 层另一个用于 UI 层拥有的IProductService 抽象。然而,出于实用主义,我们选择不为这个示例追求这条路径,因此将违规留在原地。我们希望您能理解我们不想让事情过于复杂。

This means that to fix the violation, we need an additional two extra projects in our solution — one for the isolated UI layer without the Composition Root and another for the IProductService Abstraction that the UI layer owns. Out of pragmatism, however, we chose not to pursuit this path for this example and, therefore, leave the violation in place. We hope you can appreciate that we don’t want to overcomplicate things.

接口还是抽象类?

Interfaces or abstract classes?

许多面向对象的设计指南都将接口作为主要的抽象机制,而 .NET Framework 设计指南认可抽象类优于接口。9  你应该使用接口还是抽象类?关于 DI,令人放心的答案是这无关紧要。重要的部分是您针对某种抽象进行编程。

Many guides to object-oriented design focus on interfaces as the main abstraction mechanism, whereas the .NET Framework Design Guidelines endorse abstract classes over interfaces.9  Should you use interfaces or abstract classes? With relation to DI, the reassuring answer is that it doesn’t matter. The important part is that you program against some sort of abstraction.

在其他情况下,在接口和抽象类之间进行选择很重要,但在这里不重要。您会注意到我们可以互换使用这些词;我们经常使用术语抽象来涵盖接口和抽象类。这并不意味着作为作者,我们对其中一个没有偏爱。事实上,我们有。在编写应用程序时,我们通常更喜欢接口而不是抽象类,原因如下:

Choosing between interfaces and abstract classes is important in other contexts, but not here. You’ll notice that we use these words interchangeably; we often use the term Abstraction to encompass both interfaces and abstract classes. This doesn’t mean that we, as authors, don’t have a preference for one over the other. We do, in fact. When it comes to writing applications, we typically prefer interfaces over abstract classes for these reasons:

  • 抽象类很容易被滥用为基类。基类可以很容易地变成不断变化、不断增长的上帝对象. 10  派生类与其基类紧密耦合,当基类包含Volatile行为时,这会成为一个问题。另一方面,接口迫使我们进入“组合优于继承”的口头禅。11 
  • Abstract classes can easily be abused as base classes. Base classes can easily turn into ever-changing, ever-growing God Objects.10  The derivatives are tightly coupled to its base class, which can become a problem when the base class contains Volatile behavior. Interfaces, on the other hand, force us into the “Composition over Inheritance” mantra.11 
  • 具体类可以实现多个接口,尽管在 .NET 中,这些接口只能从单个基类派生。使用接口作为抽象的载体更加灵活。
  • Concrete classes can implement several interfaces, although in .NET, those can only derive from a single base class. Using interfaces as the vehicle of Abstraction is more flexible.
  • 与抽象类相比,C# 中的接口定义没有那么笨拙。对于接口,我们可以省略其成员中的和关键字。这使得接口的定义更加简洁。abstractpublic
  • Interface definitions in C# are less clumsy compared to abstract classes. With interfaces, we can omit the abstract and public keywords from their members. This makes an interface a more succinct definition.

然而,在编写可重用库时,由于需要处理向后兼容性,这个主题变得不那么明确了。从这个角度来看,抽象类可能更有意义,因为可以在以后添加非抽象成员,而将成员添加到接口是一个重大变化。这就是 .NET Framework 设计指南更喜欢抽象类的原因。

When writing reusable libraries, however, the subject is becoming less clear-cut, due to the need to deal with backward compatibility. In that light, an abstract class might make more sense because non-abstract members can be added later, whereas adding members to an interface is a breaking change. That’s why the .NET Framework Design Guidelines prefer abstract classes.

现在让我们转到数据访问层。我们将为先前定义的IProductRepository接口创建一个实现。

Now let’s move on to the data access layer. We’ll create an implementation for the previously defined IProductRepository interface.

3.1.3 构建新的数据访问层

3.1.3 Building a new data access layer

像 Mary 一样,我们希望使用 Entity Framework Core 来实现我们的数据访问层,因此我们按照她在第 2 章中执行的相同步骤来创建实体模型。主要区别在于现在只是数据访问层的一个实现细节,而不是整个数据访问层。CommerceContext

Like Mary, we’d like to implement our data access layer using Entity Framework Core, so we follow the same steps she did in chapter 2 to create the Entity model. The main difference is that CommerceContext is now only an implementation detail of the data access layer, as opposed to being the entirety of the data access layer.

在此模型中,数据访问层之外的任何内容都不会知道或依赖实体框架。它可以在没有任何上游影响的情况下被换出。考虑到这一点,我们可以创建.IProductRepository

In this model, nothing outside of the data access layer will have any awareness of, or dependency on, Entity Framework. It can be swapped out without any upstream effects. With that in mind, we can create an implementation of IProductRepository.

清单 3.10IProductRepository使用 Entity Framework Core 实现

Listing 3.10 Implementing IProductRepository using Entity Framework Core

public class SqlProductRepository : IProductRepository
{
    private readonly CommerceContext context;

    public SqlProductRepository(CommerceContext context)
    {
        if (context == null) throw new ArgumentNullException("context");

        this.context = context;
    }

    public IEnumerable<Product> GetFeaturedProducts()
    {
        return
            from product in this.context.Products
            where product.IsFeatured
            select product;
    }
}

在 Mary 的应用程序中,Product 实体也被用作域对象,尽管它是在数据访问层中定义的。这已不再是这种情况。该类Product现在已在我们的领域层中定义。我们的数据访问层重用了Product从那层。

In Mary’s application, the Product Entity was also used as a domain object, although it was defined in the data access layer. This is no longer the case. The Product class is now defined in our domain layer. Our data access layer reuses the Product class from that layer.

为简单起见,我们选择让数据访问层重用我们的领域对象,而不是定义自己的实现。我们之所以能够这样做,是因为 Entity Framework Core 允许我们编写无持久性的实体12  这是否是一种合理的做法在很大程度上取决于域对象的结构和复杂性。如果我们后来得出结论,这个共享模型正在对我们的模型强制执行不需要的约束,我们可以通过引入内部持久性对象来更改我们的数据访问层,而无需触及应用程序的其余部分。在那种情况下,我们需要数据访问层将那些内部持久性对象转换为域对象。

For simplicity, we chose to let the data access layer reuse our domain object instead of defining its own implementation. We were able to do so because Entity Framework Core allows us to write Entities that are persistence ignorant.12  Whether this is a reasonable practice depends a lot on the structure and complexity of your domain objects. If we later conclude that this shared model is enforcing unwanted constraints on our model, we can change our data access layer by introducing internal persistence objects, without touching the rest of the application. In that case, we’d need the data access layer to convert those internal persistence objects into domain objects.

在上一章中,我们讨论了 MaryCommerceContext对连接字符串的隐式依赖是如何导致她一路走来的问题的。我们的 newCommerceContext将使这种依赖关系显式化,这是与 Mary 的实现的另一个偏差。下一个清单显示了我们的新CommerceContext.

In the previous chapter, we discussed how the implicit dependency of Mary’s CommerceContext on the connection string caused her problems along the way. Our new CommerceContext will make this dependency explicit, which is another deviation from Mary’s implementation. The next listing shows our new CommerceContext.

清单 3.11 一个更好的CommerceContext

Listing 3.11 A better CommerceContext class

public class CommerceContext : DbContext
{
    private readonly string connectionString;

    public CommerceContext(string connectionString)    ①  
    {
        if (string.IsNullOrWhiteSpace(connectionString))
            throw new ArgumentException(
                "connectionString should not be empty.",
                "connectionString");

        this.connectionString = connectionString;    ②  
    }

    public DbSet<Product> Products { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder builder)
    {
        builder.UseSqlServer(this.connectionString);
    }
}

这几乎让我们结束了电子商务应用程序的重新实现。唯一仍然缺少的实现是IUserContext.

This almost brings us to the end of our re-implementation of the e-commerce application. The only implementation still missing is that of IUserContext.

3.1.4 实现特定于 ASP.NET Core 的IUserContext适配器

3.1.4 Implementing an ASP.NET Core–specific IUserContext Adapter

缺少的最后一个具体实现是. 在 Web 应用程序中,有关发出请求的用户的信息通常会随每个请求一起传递到服务器。此信息使用 cookie 或 HTTP 标头进行中继。我们如何检索当前用户的身份在很大程度上取决于我们使用的框架。这意味着我们在构建 ASP.NET Core 应用程序时需要完全不同的实现,例如与 Windows 服务相比。IUserContext

The last concrete implementation missing is that of IUserContext. In web applications, information about a user who issues a request is usually passed on to the server with each request. This information is relayed using cookies or HTTP headers. How we retrieve the identity of the current user is highly dependent on the framework we use. This means that we’ll need a completely different implementation when building an ASP.NET Core application compared with, for instance, a Windows service.

我们的实施IUserContext是特定于框架的。我们不希望我们的领域层和数据层都知道有关应用程序框架的任何信息。这将使得无法在不同的上下文中使用这些层。我们需要在其他地方实施。因此,UI 层是我们IUserContext实现的理想场所。

The implementation of our IUserContext is framework specific. We want neither our domain layer nor our data layer to know anything about the application framework. That would make it impossible to use those layers in a different context. We need to implement this elsewhere. The UI layer, therefore, is an ideal place for our IUserContext implementation.

以下清单显示了IUserContextASP.NET Core 应用程序的可能实现。

The following listing shows a possible IUserContext implementation for an ASP.NET Core application.

ASP.NET Core 的清单 3.12 IUserContext实现

Listing 3.12 IUserContext implementation for ASP.NET Core

public class AspNetUserContextAdapter : IUserContext
{
    private static HttpContextAccessor Accessor = new HttpContextAccessor();

    public bool IsInRole(Role role)
    {
        return Accessor.HttpContext.User.IsInRole(role.ToString());
    }
}

AspNetUserContextAdapter需要一个工作。,一个由 ASP.NET Core 框架指定的组件,允许访问当前请求的 ,就像我们能够在 ASP.NET “经典”中使用. 我们用来访问有关当前用户的请求信息。HttpContextAccessorHttpContextAccessorHttpContextHttpContext.CurrentHttpContext

AspNetUserContextAdapter requires an HttpContextAccessor to work. HttpContextAccessor, a component specified by the ASP.NET Core framework, allows access to the HttpContext of the current request, like we were able to in ASP.NET “classic” using HttpContext.Current. We use HttpContext to access the request’s information about the current user.

AspNetUserContextAdapter使我们特定于应用程序的IUserContext 抽象适应ASP.NET Core API。此类是我们在第 1 章中讨论的适配器设计模式的实现。13 

AspNetUserContextAdapter adapts our application-specific IUserContext Abstraction to the ASP.NET Core API. This class is an implementation of the Adapter design pattern that we discussed in chapter 1.13 

实施后,我们对AspNetUserContextAdapter电子商务应用程序的重新实施就完成了。这将我们带到了Composition Root

With AspNetUserContextAdapter implemented, our reimplementation of the e-commerce application is finished. This brings us to our Composition Root.

3.1.5 在Composition Root中组合应用

3.1.5 Composing the application in the Composition Root

有了,和实现后,我们现在可以设置 ASP.NET Core MVC 来构建 的实例,其中由一个实例提供,该实例本身是使用一个和一个. 这最终会产生如下所示的对象图。ProductServiceSqlProductRepositoryAspNetUserContextAdapterHomeControllerHomeControllerProductServiceSqlProductRepositoryAspNetUserContextAdapter

With ProductService, SqlProductRepository and AspNetUserContextAdapter implemented, we can now set up ASP.NET Core MVC to construct an instance of HomeController, where HomeController is fed by a ProductService instance, which itself is constructed using a SqlProductRepository and an AspNetUserContextAdapter. This eventually results in an object graph that would look as follows.

清单 3.13 应用程序的对象图

Listing 3.13 The application’s object graph

new HomeController(
    new ProductService(
        new SqlProductRepository(
            new CommerceContext(connectionString)),
        new AspNetUserContextAdapter()));
03-15.tif

图 3.13 完成的应用程序的屏幕截图

Figure 3.13 Screen capture of the finished application

我们将在第 7 章中更详细地讨论如何将此类对象图的构造插入到 ASP.NET Core 框架中,因此我们不会在这里展示。但现在一切都正确连接在一起,我们可以浏览到应用程序的主页并获得如图 3.13所示的页面。

We’ll discuss how the construction of such an object graph is plugged into the ASP.NET Core framework in greater detail in chapter 7, so we won’t show that here. But now that everything is correctly wired together, we can browse to the application’s homepage and get the page shown in figure 3.13.

3.2 分析松耦合实现

3.2 Analyzing the loosely coupled implementation

上一节包含很多细节,因此如果您一路上看不到大局也就不足为奇了。在本节中,我们将尝试从更广泛的角度解释发生的事情。

The previous section contained lots of details, so it’s hardly surprising if you lost sight of the big picture along the way. In this section, we’ll try to explain what happened in broader terms.

3.2.1 理解组件之间的交互

3.2.1 Understanding the interaction between components

每一层中的类直接或以抽象形式相互交互。它们跨模块边界这样做,因此很难了解它们是如何交互的。图 3.14显示了不同的依赖关系如何交互,对图 3.4中描述的原始大纲进行了更详细的概述。

The classes in each layer interact with each other either directly or in abstract form. They do so across module boundaries, so it can be difficult to follow how they interact. Figure 3.14 shows how the different Dependencies interact, giving a more detailed overview to the original outline described in figure 3.4.

03-16.eps

图 3.14 电子商务应用中 DI 涉及的元素之间的交互

Figure 3.14 Interaction between elements involved in DI in the e-commerce application

当应用程序启动时,中的代码Startup创建一个新的自定义控制器激活器并从应用程序的配置文件中查找连接字符串。当页面请求进来时,应用程序调用Create控制器激活器。

When the application starts, the code in Startup creates a new custom controller activator and looks up the connection string from the application’s configuration file. When a page request comes in, the application invokes Create on the controller activator.

激活器将存储的连接字符串提供给新的实例(图中未显示)。它注入一个新的实例。反过来,实例连同的实例(图中未显示)被注入到新的实例中。同样,被注入到 的新实例中,然后从该方法返回。CommerceContextCommerceContextSqlProductRepositorySqlProductRepositoryAspNetUserContextAdapterProductServiceProductServiceHomeControllerCreate

The activator supplies the stored connection string to a new instance of CommerceContext (not shown in the diagram). It injects CommerceContext into a new instance of SqlProductRepository. In turn, the SqlProductRepository instance together with an instance of AspNetUserContextAdapter (not shown in the diagram) are injected into a new instance of ProductService. Similarly, ProductService is injected into a new instance of HomeController, which is then returned from the Create method.

然后 ASP.NET Core MVC 框架调用实例Index上的方法HomeController,使其调用实例GetFeaturedProducts上的方法。这又会调用实例上的方法。最后,填充后的 返回,MVC 找到并呈现正确的视图。ProductRepositoryGetFeaturedProductsSqlProductRepositoryViewResultFeaturedProductsViewModel

The ASP.NET Core MVC framework then invokes the Index method on the HomeController instance, causing it to invoke the GetFeaturedProducts method on the ProductRepository instance. This in turn calls the GetFeaturedProducts method on the SqlProductRepository instance. Finally, the ViewResult with the populated FeaturedProductsViewModel is returned, and MVC finds and renders the correct view.

3.2.2 分析新的依赖图

3.2.2 Analyzing the new dependency graph

在 2.2 节中,您了解了依赖关系图如何帮助您分析和理解架构实现所提供的灵活性程度。DI 是否更改了应用程序的依赖关系图?

In section 2.2, you saw how a dependency graph can help you analyze and understand the degree of flexibility provided by the architectural implementation. Has DI changed the dependency graph for the application?

图 3.15显示依赖图确实发生了变化。领域模型不再有任何依赖关系,可以作为一个独立的模块。另一方面,数据访问层现在有了依赖;在玛丽的申请中,它没有。

Figure 3.15 shows that the dependency graph has indeed changed. The domain model no longer has any dependencies and can act as a standalone module. On the other hand, the data access layer now has a dependency; in Mary’s application, it had none.

03-17.eps

图 3.15 显示应用了 DI 的示例电子商务应用程序的依赖关系图。显示了所有类和接口,以及它们之间的关系。

Figure 3.15 Dependency graph showing the sample e-commerce application with DI applied. All classes and interfaces are shown, as well as their relationships to one another.

图 3.15中需要注意的最重要的事情是领域层不再有任何依赖关系。这应该提高我们的希望,我们这次可以更有利地回答有关可组合性的原始问题(参见第 2.2 节):

The most important thing to note in figure 3.15 is that the domain layer no longer has any dependencies. This should raise our hopes that we can answer the original questions about composability (see section 2.2) more favorably this time:

  • 我们可以更换基于网络的用户界面吗使用基于 WPF 的 UI这在以前是可能的,并且在新设计中仍然是可能的。域模型库和数据访问库都不依赖于基于 Web 的 UI,因此我们可以轻松地在其位置放置其他内容。
  • Can we replace the web-based UI with a WPF-based UI? That was possible before and is still possible with the new design. Neither the domain model library nor the data access library depends on the web-based UI, so we can easily put something else in its place.
  • 我们能否将关系数据访问层替换为与 Azure 表服务一起使用的层?在后面的章节中,我们将描述应用程序如何定位和实例化正确的,因此,现在,从表面上看:数据访问层正在通过后期绑定加载,并且类型名称被定义为应用程序在应用程序的配置文件中设置。可以抛弃当前的数据访问层并注入一个新的,只要它还提供.IProductRepositoryIProductRepository
  • Can we replace the relational data access layer with one that works with the Azure Table Service? In a later chapter, we’ll describe how the application locates and instantiates the correct IProductRepository, so, for now, take the following at face value: the data access layer is being loaded by late binding, and the type name is defined as an application setting in the application’s configuration file. It’s possible to throw the current data access layer away and inject a new one, as long as it also provides an implementation of IProductRepository.

本章中描述的示例电子商务应用程序只向我们展示了有限的复杂程度:只读场景中只涉及一个存储库。到目前为止,我们一直保持应用程序尽可能简单和小巧,以温和地介绍一些核心概念和原则。因为 DI 的主要目的之一是管理复杂性,所以我们需要一个复杂的应用程序来充分领会它的力量。在本书的学习过程中,我们将扩展示例电子商务应用程序以充分展示 DI 的不同方面。

The sample e-commerce application described in this chapter only presents us with a limited level of complexity: there’s only a single Repository involved in a read-only scenario. Until now, we’ve kept the application as simple and small as possible to gently introduce some core concepts and principles. Because one of the main purposes of DI is to manage complexity, we need a complex application to fully appreciate its power. During the course of the book, we’ll expand the sample e-commerce application to fully demonstrate different aspects of DI.

本章结束了本书的第一部分。第 1 部分的目的是让 DI 广为人知,并从总体上介绍 DI。在本章中,您看到了构造函数注入的示例。我们还介绍了Method InjectionComposition Root作为与 DI 相关的模式。在下一章中,我们将深入探讨这些和其他设计模式。

This chapter concludes the first part of the book. The purpose of part 1 was to put DI on the map and to introduce DI in general. In this chapter, you’ve seen examples of Constructor Injection. We also introduced Method Injection and Composition Root as patterns related to DI. In the next chapter, we’ll dive deeper into these and other design patterns.

概括

Summary

  • 将现有应用程序重构为更易于维护、松散耦合的设计是很困难的。另一方面,大的重写通常风险更大且成本更高。
  • Refactoring existing applications towards a more maintainable, loosely coupled design is hard. Big rewrites, on the other hand, are often riskier and expensive.
  • 使用视图模型可以简化视图,因为传入的数据是专门为视图设计的。
  • The use of view models can simplify the view, because the incoming data is shaped specifically for the view.
  • 因为视图更难测试,所以视图越笨越好。它还简化了可能处理视图的 UI 设计人员的工作。
  • Because views are harder to test, the dumber the view, the better. It also simplifies the work of a UI designer who might work on the view.
  • 当您限制域层内易失性依赖项的数量时,您将获得更高程度的解耦、重用和可测试性。
  • When you limit the amount of Volatile Dependencies within the domain layer, you get a higher degree of decoupling, reuse, and Testability.
  • 在构建应用程序时,由外而内的方法有助于更快速地制作原型,从而缩短反馈周期。
  • When building applications, the outside-in approach facilitates more rapid prototyping, which can shorten the feedback cycle.
  • 当您希望在应用程序中实现高度模块化时,您需要应用构造函数注入模式并在靠近应用程序入口点的组合根中构建对象图。
  • When you want a high degree of modularity in your application, you need to apply the Constructor Injection pattern and build object graphs in the Composition Root, which is located close to the application’s entry point.
  • 接口编程是 DI 的基石。它允许您替换、模拟和拦截依赖项,而无需对其使用者进行更改。当实现和抽象放在不同的程序集中时,它可以替换整个库。
  • Programming to interfaces is a cornerstone of DI. It allows you to replace, mock, and Intercept a Dependency, without having to make changes to its consumers. When implementation and Abstraction are placed in different assemblies, it enables whole libraries to be replaced.
  • 接口编程并不意味着所有的类都应该实现一个接口。实体、视图模型和 DTO等短期对象通常不包含需要模拟、拦截、修饰或替换的行为。
  • Programming to interfaces doesn’t mean that all classes should implement an interface. Short-lived objects, such as Entities, view models, and DTOs, typically contain no behavior that requires mocking, Interception, decoration, or replacement.
  • 对于 DI,使用接口还是纯抽象类都没有关系。从一般开发的角度来看,作为作者,我们通常更喜欢接口而不是抽象类。
  • With respect to DI, it doesn’t matter whether you use interfaces or purely abstract classes. From a general development perspective, as authors, we typically prefer interfaces over abstract classes.
  • 重用库是具有在编译时未知的客户端的库。可重用库通常通过 NuGet 提供。只有同一 (Visual Studio) 解决方案中的调用方的库不被视为可重用库。
  • A reusable library is a library that has clients that aren’t known at compile time. Reusable libraries are typically shipped via NuGet. Libraries that only have callers within the same (Visual Studio) solution aren’t considered to be reusable libraries.
  • DI 与依赖倒置原则密切相关。这个原则意味着你应该针对接口进行编程,并且一个层必须控制它使用的接口。
  • DI is closely related to the Dependency Inversion Principle. This principle implies that you should program against interfaces, and that a layer must be in control over the interfaces it uses.
  • 使用DI 容器有助于使应用程序的组合根更易于维护,但它不会神奇地使紧密耦合的代码松散耦合。为了使应用程序变得可维护,必须在设计时考虑到 DI 模式和技术。
  • The use of a DI Container can help in making the application’s Composition Root more maintainable, but it won’t magically make tightly coupled code loosely coupled. For an application to become maintainable, it must be designed with DI patterns and techniques in mind.

第 2 部分

目录

Part 2

Catalog

P第 1 条概述了 DI,讨论了 DI 的目的和好处。尽管第 3 章包含了一个广泛的示例,但我们确信第一章仍然给您留下了一些未解决的问题。在第 2 部分中,我们将更深入地挖掘以回答其中的一些问题。

Part 1 provided an overview of DI, discussing the purpose and benefits of DI. Even though chapter 3 contained an extensive example, we’re sure the first chapters still left you with some unresolved questions. In part 2, we’ll dig a little deeper to answer some of those questions.

正如标题所暗示的,第 2 部分提供了模式、反模式和代码味道的完整目录。有些人不喜欢设计模式,因为他们觉得它们枯燥或过于抽象。就个人而言,我们喜欢模式,因为它们为我们提供了一种高级语言,使我们在讨论软件设计时更加高效和简洁。我们的目的是使用这个目录为 DI 提供一种模式语言。尽管模式描述必须包含一些概括,但我们使用示例使每个模式具体化。您可以按顺序阅读所有三章,但目录中的每个项目也都已写好,以便您可以单独阅读。

As the title implies, part 2 presents a complete catalog of patterns, anti-patterns, and code smells. Some people dislike design patterns, because they find them dry or too abstract. Personally, we love patterns, because they provide us with a high-level language that makes us more efficient and concise when we discuss software design. It’s our intent to use this catalog to provide a pattern language for DI. Although a pattern description must contain some generalizations, we’ve made each pattern concrete, using examples. You can read all three chapters in sequence, but each item in the catalog is also written so that you can read it by itself.

第 4 章包含 DI 设计模式的迷你目录。从某种意义上说,这些模式构成了有关如何实施 DI 的规范性指导,但您应该知道,我们并不认为它们具有同等重要性。Constructor InjectionComposition Root是迄今为止最重要的设计模式,而所有其他模式应视为可以在特殊情况下应用的边缘案例。

Chapter 4 contains a mini catalog of DI design patterns. In a sense, these patterns constitute prescriptive guidance on how to implement DI, but you should be aware that we don’t consider them to be of equal importance. Constructor Injection and Composition Root are by far the most important design patterns, whereas all the other patterns should be treated as fringe cases that can be applied in specialized circumstances.

第 4 章为您提供了一组通用的解决方案,而第 5 章包含了一系列应避免的情况。这些反模式描述了解决典型 DI 挑战的常见但不正确的方法。在每种情况下,反模式都描述了如何识别事件以及如何解决问题。了解和理解这些反模式对于避免它们所代表的陷阱非常重要,而且正如第 4 章介绍的两个最重要的模式一样,最重要的反模式是Service Locator,它是 DI 的对立面。

Whereas chapter 4 gives you a set of generalized solutions, chapter 5 contains a catalog of situations to avoid. These anti-patterns describe common, but incorrect ways to address typical DI challenges. In each case, the anti-pattern describes how to identify occurrences and how to resolve the issue. It’s important to know and understand these anti-patterns to avoid the traps that they represent, and, just as chapter 4 presents two dominatingly important patterns, the most important anti-pattern is Service Locator, the antithesis of DI.

当您将 DI 应用于现实生活中的编程任务时,您会遇到一些挑战。我们认为我们都曾有过怀疑自己是否理解某个工具的时刻或技术,但我们认为,“理论上,这可能有效,但我的情况很特殊。” 当我们发现自己有这样的想法时,我们很清楚我们还有更多东西要学。

As you apply DI to real-life programming tasks, you’ll run into some challenges. We think we’ve all had moments of doubt where we feel that we understand a tool or technique, and yet we think, “In theory, this may work, but my case is special.” When we find ourself thinking like this, it’s clear to us that we have more to learn.

在我们的职业生涯中,我们看到了一组特定的问题一再出现。这些问题中的每一个都有一个通用的解决方案,您可以将您的代码应用到第 4 章中的一种 DI 模式。第 6 章包含这些常见问题或代码异味及其相应解决方案的目录。

During our career, we’ve seen a particular set of problems appear again and again. Each of these problems has a general solution you can apply to move your code towards one of the DI patterns from chapter 4. Chapter 6 contains a catalog of these common problems, or code smells, and their corresponding solutions.

我们希望这是本书中最有用的部分,因为它最经久不衰。希望您在第一次阅读这些章节数月甚至数年之后能够再次阅读这些章节。

We expect this to be the most useful part of the book, because it’s the most enduring. Hopefully, you’ll return to these chapters months and even years after you first read them.

4

种DI模式

4

DI patterns

在这一章当中

In this chapter

  • 使用Composition Root组合对象图
  • Composing object graphs with Composition Root
  • 使用构造函数注入静态声明所需的依赖项
  • Statically declaring required Dependencies with Constructor Injection
  • 使用方法注入将依赖项传递到组合根之外
  • Passing Dependencies outside the Composition Root with Method Injection
  • 使用属性注入声明可选依赖项
  • Declaring optional Dependencies with Property Injection
  • 了解要使用的模式
  • Understanding which pattern to use

像所有专业人士一样,厨师有自己的行话,使他们能够用一种对我们其他人来说通常听起来深奥的语言来交流复杂的食物准备工作。他们使用的大多数术语都基于法语(除非您已经会说法语),这无济于事。酱汁是厨师使用专业术语的一个很好的例子。在第 1 章中,我们简要讨论了sauce béarnaise,但没​​有详细说明围绕它的分类法。

Like all professionals, cooks have their own jargon that enables them to communicate about complex food preparation in a language that often sounds esoteric to the rest of us. It doesn’t help that most of the terms they use are based on the French language (unless you already speak French, that is). Sauces are a great example of the way cooks use their professional terminology. In chapter 1, we briefly discussed sauce béarnaise, but we didn’t elaborate on the taxonomy that surrounds it.

蛋黄酱实际上是一种荷兰,其中柠檬汁被减少的醋、青葱、山萝卜和龙蒿所取代。其他酱料是基于酱汁荷兰酱,包括马克最喜欢的酱汁慕斯林,它是将打发的奶油倒入荷兰酱中制成的

A sauce béarnaise is really a sauce hollandaise where the lemon juice is replaced by a reduction of vinegar, shallots, chervil, and tarragon. Other sauces are based on sauce hollandaise, including Mark’s favorite, sauce mousseline, which is made by folding whipped cream into the hollandaise.

你注意到行话了吗?我们没有说“小心地将生奶油混合到酱汁中,注意不要使其塌陷”,而是使用了折叠这个词. 我们没有说“加厚和加强醋的味道”,而是使用了减少这个词。行话使您能够简洁有效地进行交流。

Did you notice the jargon? Instead of saying, “carefully mix the whipped cream into the sauce, taking care not to collapse it,” we used the term folding. Instead of saying, “thickening and intensifying the flavor of vinegar,” we used the term reduction. Jargon allows you to communicate concisely and effectively.

在软件开发中,我们有一套复杂而难以理解的行话。您可能不知道烹饪术语bain-marie指的是什么,但我们敢肯定,如果您告诉大多数厨师“字符串是不可变的类,它表示 Unicode 字符的序列”,他们会完全不知所措。当谈到如何构建代码来解决特定类型的问题时,我们有为常见解决方案命名的设计模式。就像sauce hollandaisefold这两个术语帮助我们简洁地交流如何制作sauce mousseline一样,设计模式帮助我们谈论代码的结构。

In software development, we have a complex and impenetrable jargon of our own. You may not know what the cooking term bain-marie refers to, but we’re pretty sure most chefs would be utterly lost if you told them that “strings are immutable classes, which represent sequences of Unicode characters.” And when it comes to talking about how to structure code to solve particular types of problems, we have design patterns that give names to common solutions. In the same way that the terms sauce hollandaise and fold help us succinctly communicate how to make sauce mousseline, design patterns help us talk about how code is structured.

在前面的章节中,我们已经命名了很多软件设计模式。例如,在第 1 章中我们讨论了模式抽象工厂、空对象、装饰器、组合、适配器、保护子句、存根、模拟和假。虽然此时您可能无法回忆起它们中的每一个,但如果我们谈论设计模式,您可能不会感到那么不舒服。我们人类喜欢命名重复出现的模式,即使它们很简单。

We’ve already named quite a few software design patterns in the previous chapters. For instance, in chapter 1 we talked about the patterns Abstract Factory, Null Object, Decorator, Composite, Adapter, Guard Clause, Stub, Mock, and Fake. Although, at this point, you might not be able to recall each of them, you probably won’t feel that uncomfortable if we talk about design patterns. We human beings like to name reoccurring patterns, even if they’re simple.

如果您对设计模式的一般了解有限,请不要担心。设计模式的主要目的是提供对实现目标的特定方式的详细且独立的描述——如果您愿意的话,也可以是配方。此外,您已经看到了我们将在本章中描述的四种基本 DI 设计模式中的三种示例:

Don’t worry if you have only a limited knowledge of design patterns in general. The main purpose of a design pattern is to provide a detailed and self-contained description of a particular way of attaining a goal — a recipe, if you will. And besides, you already saw examples of three out of the four basic DI design patterns that we’ll describe in this chapter:

  • Composition Root——描述你应该在哪里以及如何组合应用程序的对象图。
  • Composition Root — Describes where and how you should compose an application’s object graphs.
  • 构造函数注入——允许类静态声明其所需的依赖项。
  • Constructor Injection — Allows a class to statically declare its required Dependencies.
  • 方法注入——当依赖项或消费者可能因每个操作而改变,使您能够向消费者提供依赖项。
  • Method Injection — Enables you to provide a Dependency to a consumer when either the Dependency or the consumer might change for each operation.
  • 属性注入- 允许客户端有选择地覆盖某些类的默认行为,其中此默认行为在Local Default中实现。
  • Property Injection — Allows clients to optionally override some class’s default behavior, where this default behavior is implemented in a Local Default.

本章的结构是提供模式目录。对于每个模式,我们将提供简短描述、代码示例、优点和缺点等。您可以按顺序阅读本章介绍的所有四种模式,也可以只阅读您感兴趣的模式。最重要的模式是Composition RootConstructor Injection,您应该在大多数情况下使用它们——其他模式随着章节的进行变得更加专业。

This chapter is structured to provide a catalog of patterns. For each pattern, we’ll provide a short description, a code example, advantages and disadvantages, and so on. You can read about all four patterns introduced in this chapter in sequence or only read the ones that interest you. The most important patterns are Composition Root and Constructor Injection, which you should use in most situations — the other patterns become more specialized as the chapter progresses.

4.1 组合根

4.1 Composition Root

我们应该在哪里编写对象图?

Where should we compose object graphs?

尽可能靠近应用程序的入口点。

As close as possible to the application’s entry point.

当您从许多松散耦合的类创建应用程序时,组合应该尽可能靠近应用程序的入口点进行。该Main方法是大多数应用程序类型的入口点。Composition Root组成对象图,随后执行应用程序的实际工作。

When you’re creating an application from many loosely coupled classes, the composition should take place as close to the application’s entry point as possible. The Main method is the entry point for most application types. The Composition Root composes the object graph, which subsequently performs the actual work of the application.

04-01.eps

图 4.1 靠近应用程序的入口点,组合根负责组合松耦合类的对象图。Composition Root直接依赖于系统中的所有模块。

Figure 4.1 Close to the application’s entry point, the Composition Root takes care of composing object graphs of loosely coupled classes. The Composition Root takes a direct dependency on all modules in the system.

在上一章中,您看到大多数类都使用构造函数注入。通过这样做,他们将创建依赖项的责任推给了消费者。然而,这样的消费者也推动了创建他们的依赖关系的责任取决于他们的消费者。

In the previous chapter, you saw that most classes used Constructor Injection. By doing so, they pushed the responsibility for the creation of their Dependencies up to their consumers. Such consumers, however, also pushed the responsibility for creating their Dependencies up to their consumers.

您不能无限期地延迟对象的创建。必须有一个位置供您创建对象图。您应该将此创建集中到应用程序的单个区域中。这个地方叫做Composition Root

You can’t delay the creation of your objects indefinitely. There must be a location where you create your object graphs. You should concentrate this creation into a single area of your application. This place is called the Composition Root.

在上一章中,这导致了您在清单 3.13(图 4.1)中看到的对象图。此清单还显示来自所有应用程序层的所有组件都在Composition Root中构建。

In the previous chapter, this resulted in the object graph that you saw in listing 3.13 (figure 4.1). This listing also shows that all components from all application layers are constructed in the Composition Root.

清单 4.1 来自第 3 章的应用程序对象图

Listing 4.1 The application’s object graph from chapter 3

new HomeController(    ①  
    new ProductService(    ②  
        new SqlProductRepository(    ③  
            new CommerceContext(connectionString)),  ③  
        new AspNetUserContextAdapter()));    ④  

如果您有一个控制台应用程序是为在这个特定的对象图上运行而编写的,它可能看起来如下面的清单所示。

If you were to have a console application that was written to operate on this particular object graph, it might look as shown in the following listing.

清单 4.2 作为控制台应用程序一部分的应用程序对象图

Listing 4.2 The application’s object graph as part of a console application

public static class Program
{
    public static void Main(string[] args)    ①  
    {
        string connectionString = args[0];    ②  

        HomeController controller =
            CreateController(connectionString);    ③  

        var result = controller.Index();

        var vm = (FeaturedProductsViewModel)result.Model;

        Console.WriteLine("Featured products:");

        foreach (var product in vm.Products)
        {
            Console.WriteLine(product.SummaryText);
        }
    }

    private static HomeController CreateController(    ④  
        string connectionString)
    {
        var userContext = new ConsoleUserContext();    ⑤  

        return
            new HomeController(    ⑥  
                new ProductService(    ⑥  
                    new SqlProductRepository(    ⑥  
                        new CommerceContext(    ⑥  
                            connectionString)),    ⑥  
                    userContext));    ⑥  
    }
}

在此示例中,Composition RootMain与方法分离. 然而,这不是必需的 — Composition Root不是方法或类,它是一个概念。它可以是Main方法的一部分,也可以跨越多个类,只要它们都驻留在一个模块中即可。将其分离到自己的方法中有助于确保组合得到巩固,并且不会以其他方式散布在后续的应用程序逻辑中。

In this example, the Composition Root is separated from the Main method. This isn’t required, however — the Composition Root isn’t a method or a class, it’s a concept. It can be part of the Main method, or it can span multiple classes, as long as they all reside in a single module. Separating it into its own method helps to ensure that the composition is consolidated and not otherwise interspersed with subsequent application logic.

4.1.1 Composition Root 的工作原理

4.1.1 How Composition Root works

当您编写松散耦合的代码时,您会创建许多类来创建一个应用程序。在许多不同的位置组合这些类以创建小型子系统可能很诱人,但这限制了您拦截的能力那些系统来修改他们的行为。相反,您应该在应用程序的一个区域中组合类。

When you write loosely coupled code, you create many classes to create an application. It can be tempting to compose these classes at many different locations in order to create small subsystems, but that limits your ability to Intercept those systems to modify their behavior. Instead, you should compose classes in one single area of your application.

当您孤立地看待构造函数注入时,您可能会想,它不会将选择依赖关系的决定推迟到另一个地方吗?是的,确实如此,这是一件好事。这意味着您将获得一个可以连接协作类的中心位置。

When you look at Constructor Injection in isolation, you may wonder, doesn’t it defer the decision about selecting a Dependency to another place? Yes, it does, and that’s a good thing. This means that you get a central place where you can connect collaborating classes.

Composition Root充当第三方,将消费者与其服务联系起来。你推迟决定如何连接课程的时间越长,你的选择就越多。因此,Composition Root应放置在尽可能靠近应用程序入口点的位置。

The Composition Root acts as a third party that connects consumers with their services. The longer you defer the decision on how to connect classes, the more you keep your options open. Thus, the Composition Root should be placed as close to the application’s entry point as possible.

即使是使用松散耦合和后期绑定来组合自身的模块化应用程序也有一个包含应用程序入口点的根。示例如下:

Even a modular application that uses loose coupling and late binding to compose itself has a root that contains the entry point into the application. Examples follow:

  • .NET Core 控制台应用程序是一个包含Program类的库 (.dll)用一种Main方法。
  • A .NET Core console application is a library (.dll) containing a Program class with a Main method.
  • 一个 ASP.NET Core Web 应用程序也是一个包含带有方法的Program类的库Main.
  • An ASP.NET Core web application also is a library containing a Program class with a Main method.
  • UWP和 WPF 应用程序是可执行文件(.exe) 与 App.xaml.cs 文件。
  • UWP and WPF applications are executables (.exe) with an App.xaml.cs file.

存在许多其他技术,但它们有一个共同点:一个模块包含应用程序的入口点——这是应用程序的根。不要误以为Composition Root是 UI 层的一部分。即使您将Composition Root放置在与 UI 层相同的程序集中,正如我们将在下一个示例中所做的那样,Composition Root也不是该层的一部分。

Many other technologies exist, but they have one thing in common: one module contains the entry point of the application — this is the root of the application. Don’t be misled into thinking that the Composition Root is part of your UI layer. Even if you place the Composition Root in the same assembly as your UI layer, as we’ll do in the next example, the Composition Root isn’t part of that layer.

程序集是一个部署工件:您将代码拆分为多个程序集以允许单独部署代码。另一方面,架构层是逻辑工件:您可以将多个逻辑工件组合在一个部署工件中。尽管包含组合根和 UI 层的程序集依赖于系统中的所有其他模块,但 UI 层本身并不依赖。

Assemblies are a deployment artifact: you split code into multiple assemblies to allow code to be deployed separately. An architectural layer, on the other hand, is a logical artifact: you can group multiple logical artifacts in a single deployment artifact. Even though the assembly that holds both the Composition Root and the UI layer depends on all other modules in the system, the UI layer itself doesn’t.

不需要将Composition Root放置在与 UI 层相同的项目中。您可以将 UI 层移出应用程序的根项目。这样做的好处是你可以防止拥有 UI 层的项目产生依赖(例如,第 3 章中的数据访问层项目)。这使得 UI 类不可能意外地依赖于数据访问类。然而,这种方法的缺点是它并不总是容易做到。例如,使用 ASP.NET Core MVC,将控制器和视图模型移动到一个单独的项目是微不足道的,但对视图和客户端资源执行相同的操作可能非常具有挑战性。

It’s not a requirement for the Composition Root to be placed in the same project as your UI layer. You can move the UI layer out of the application’s root project. The advantage of this is that you can prevent the project that holds the UI layer from taking on a dependency (for instance, the data access layer project in chapter 3). This makes it impossible for UI classes to accidentally depend on data access classes. The downside of this approach, however, is that it isn’t always easy to do. With ASP.NET Core MVC, for instance, it’s trivial to move controllers and view models to a separate project, but it can be quite challenging to do the same with your views and client resources.

将表示技术与组合根分开可能也不是那么有益,因为组合根是特定于应用程序的。Composition Roots不被重用。

Separating the presentation technology from the Composition Root might not be that beneficial, either, because a Composition Root is specific to the application. Composition Roots aren’t reused.

您不应尝试在任何其他模块中组合类,因为这种方法会限制您的选择。应用程序模块中的所有类都应该使用构造函数注入(或者,在极少数情况下,使用本章其他两种模式之一),然后将其留给组合根来组合应用程序的对象图。任何使用中的DI 容器都应限制在Composition Root中。

You shouldn’t attempt to compose classes in any of the other modules, because that approach limits your options. All classes in application modules should use Constructor Injection (or, in rare cases, one of the other two patterns from this chapter), and then leave it up to the Composition Root to compose the application’s object graph. Any DI Container in use should be limited to the Composition Root.

在应用程序中,复合根应该是唯一知道构造对象图结构的地方。应用程序代码不仅放弃了对其依赖项的控制,还放弃了有关其依赖项的知识。集中这些知识可以简化开发。这也意味着应用程序代码无法将依赖项传递给与当前操作并行运行的其他线程,因为消费者无法知道这样做是否安全。相反,当分离并发操作时,组合根的工作是为每个并发操作创建一个新的对象图。

In an application, the Composition Root should be the sole place that knows about the structure of the constructed object graphs. Application code not only relinquishes control over its Dependencies, it also relinquishes knowledge about its Dependencies. Centralizing this knowledge simplifies development. This also means that application code can’t pass on Dependencies to other threads that run parallel to the current operation, because a consumer has no way of knowing whether it’s safe to do so. Instead, when spinning off concurrent operations, it’s the job of the Composition Root to create a new object graph for each concurrent operation.

清单 4.2中的Composition Root显示了Pure DI的示例。然而,组合根模式同时适用于纯 DIDI 容器。在下一节中,我们将描述如何在Composition Root中使用DI Container

The Composition Root in listing 4.2 showed an example of Pure DI. The Composition Root pattern, however, is both applicable to Pure DI and DI Containers. In the next section, we’ll describe how a DI Container can be used in a Composition Root.

4.1.2在组合根中使用DI 容器

4.1.2 Using a DI Container in a Composition Root

如第 3 章所述,DI 容器是一个软件库,可以自动执行许多涉及组合对象和管理其生命周期的任务。但它可能被滥用为服务定位器并且应该只用作组成对象图的引擎。当您从这个角度考虑DI 容器时,将其约束到Composition Root是有意义的。这也大大有利于消除DI 容器与应用程序代码库的其余部分之间的任何耦合。

As described in chapter 3, a DI Container is a software library that can automate many of the tasks involved in composing objects and managing their lifetimes. But it can be misused as a Service Locator and should only be used as an engine that composes object graphs. When you consider a DI Container from that perspective, it makes sense to constrain it to the Composition Root. This also significantly benefits the removal of any coupling between the DI Container and the rest of the application’s code base.

Composition Root可以用DI Container来实现。这意味着您使用容器在对其Resolve方法的一次调用中组成整个应用程序的对象图. 当我们与开发人员谈论这样做时,我们总是可以说这让他们感到不舒服,因为他们担心这样做效率低下并且对性能不利。你不必为此担心。几乎从来没有出现过这种情况,在出现这种情况的少数情况下,有一些方法可以解决这个问题,正如我们将在 8.4.2 节中讨论的那样。

A Composition Root can be implemented with a DI Container. This means that you use the container to compose the entire application’s object graph in a single call to its Resolve method. When we talk to developers about doing it like this, we can always tell that it makes them uncomfortable because they’re afraid that it’s terribly inefficient and bad for performance. You don’t have to worry about that. That’s almost never the case and, in the few situations where it is, there are ways to address the issue, as we’ll discuss in section 8.4.2.

不用担心使用DI 容器组合大型对象图的性能开销。这通常不是问题。在第 4 部分中,我们将深入探讨DI 容器并展示如何在Composition Root中使用DI 容器

Don’t worry about the performance overhead of using a DI Container to compose large object graphs. It’s usually not an issue. In part 4, we’ll do a deep dive into DI Containers and show how to use a DI Container inside the Composition Root.

对于基于请求的应用程序,例如网站和服务,您只需配置一次容器,然后为每个传入请求解析一个对象图。第 3 章中的电子商务 Web 应用程序就是一个例子。

When it comes to request-based applications, such as websites and services, you configure the container once, but resolve an object graph for each incoming request. The e-commerce web application in chapter 3 is an example of that.

4.1.3 示例:使用纯 DI实现合成根

4.1.3 Example: Implementing a Composition Root using Pure DI

示例电子商务 Web 应用程序必须有一个组合根来为传入的 HTTP 请求组合对象图。与所有其他 ASP.NET Core Web 应用程序一样,入口点在Main方法中. 然而,默认情况下,MainASP.NET Core 应用程序的方法将大部分工作委托给Startup. 这个Startup类对我们来说足够接近应用程序的入口点,我们将使用它作为我们的Composition Root

The sample e-commerce web application must have a Composition Root to compose object graphs for incoming HTTP requests. As with all other ASP.NET Core web applications, the entry point is in the Main method. By default, however, the Main method of an ASP.NET Core application delegates most of the work to the Startup class. This Startup class is close enough to the application’s entry point for us, and we’ll use that as our Composition Root.

与前面的控制台应用程序示例一样,我们使用Pure DI. 这意味着您使用普通的旧 C# 代码而不是DI 容器来编写对象图,如以下清单所示。

As in the previous example with the console application, we use Pure DI. This means you compose your object graphs using plain old C# code instead of a DI Container, as shown in the following listing.

清单 4.3 电子商务应用程序的Startup

Listing 4.3 The e-commerce application’s Startup class

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        this.Configuration = configuration;    ①  
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(    ②  
        IServiceCollection services)
    {
        services.AddMvc();

        services.AddHttpContextAccessor();    ③  

        var connectionString =    ④  
            this.Configuration.GetConnectionString(    ④  
                "CommerceConnection");    ④  

        services.AddSingleton<IControllerActivator>(    ⑤  
            new CommerceControllerActivator(    ⑤  
                connectionString));    ⑤  
    }

    ...
}

如果你不熟悉 ASP.NET Core,这里有一个简单的解释:Startup类是必需的;这是您应用所需管道的地方。有趣的部分是. 应用程序的整个设置都封装在类中CommerceControllerActivatorCommerceControllerActivator,我们很快就会展示。

If you’re not familiar with ASP.NET Core, here’s a simple explanation: the Startup class is a necessity; it’s where you apply the required plumbing. The interesting part is the CommerceControllerActivator. The entire setup for the application is encapsulated in the CommerceControllerActivator class, which we’ll show shortly.

要启用将 MVC 控制器连接到应用程序,您必须在 ASP.NET Core MVC 中使用适当的Seam ,称为 an (在 7.3 节中详细讨论)。现在,了解要与 ASP.NET Core MVC 集成就足够了,您必须为组合根创建一个适配器并将其告知框架。IControllerActivator

To enable wiring MVC controllers to the application, you must employ the appropriate Seam in ASP.NET Core MVC, called an IControllerActivator (discussed in detail in section 7.3). For now, it’s enough to understand that to integrate with ASP.NET Core MVC, you must create an Adapter for your Composition Root and tell the framework about it.

Startup.ConfigureServices方法_只运行一次。因此,您的类是仅初始化一次的单个实例。因为您使用自定义设置 ASP.NET Core MVC,所以MVC 调用它的方法CommerceControllerActivatorIControllerActivatorCreate为每个传入的 HTTP 请求创建一个新的控制器实例(您可以在第 7.3 节中阅读详细信息)。以下清单显示了CommerceControllerActivator.

The Startup.ConfigureServices method only runs once. As a result, your CommerceControllerActivator class is a single instance that’s only initialized once. Because you set up ASP.NET Core MVC with the custom IControllerActivator, MVC invokes its Create method to create a new controller instance for each incoming HTTP request (you can read about the details in section 7.3). The following listing shows the CommerceControllerActivator.

清单 4.4 应用程序的实现IControllerActivator

Listing 4.4 The application’s IControllerActivator implementation

public class CommerceControllerActivator : IControllerActivator
{
    private readonly string connectionString;

    public CommerceControllerActivator(string connectionString)
    {
        this.connectionString = connectionString;
    }

    public object Create(ControllerContext ctx)    ①  
    {
        Type type = ctx.ActionDescriptor
            .ControllerTypeInfo.AsType();

        if (type == typeof(HomeController))    ②  
        {
            return
                new HomeController(
                    new ProductService(
                        new SqlProductRepository(
                            new CommerceContext(
                                this.connectionString)),
                        new AspNetUserContextAdapter()));
        }
        else
        {
            throw new Exception("Unknown controller.");  ③  
        }
    }
}

注意这个例子中 的创建与我们在清单 4.1HomeController中展示的第 3 章中的应用程序对象图几乎完全相同。当 MVC 调用时,您确定控制器类型并基于此类型创建正确的对象图。Create

Notice how the creation of HomeController in this example is almost identical to the application’s object graph from chapter 3 that we showed in listing 4.1. When MVC calls Create, you determine the controller type and create the correct object graph based on this type.

04-02.eps

4.2 Composition Root分布在两个类中,但它们是在同一个模块中定义的。

Figure 4.2 The Composition Root is spread across two classes, but they’re defined within the same module.

在 2.3.3 节中,我们讨论了如何只有组合根应该依赖配置文件,因为对于可重用库来说,调用者可以强制配置它更加灵活。您还应该将配置值的加载与执行对象组合的方法分开(如清单 4.3 和 4.4 所示)。清单 4.3Startup类加载配置,而清单4.4的类仅依赖于配置值,而不依赖于配置系统。这种分离的一个重要优点是它将对象组合与使用中的配置系统分离,从而可以在不存在(有效的)配置文件的情况下进行测试。CommerceControllerActivator

In section 2.3.3, we discussed how only the Composition Root should rely on configuration files, because it’s more flexible for reusable libraries to be imperatively configurable by their callers. You should also separate the loading of configuration values from the methods that do Object Composition (as shown in listings 4.3 and 4.4). The Startup class of listing 4.3 loads the configuration, whereas the CommerceControllerActivator of listing 4.4 only depends on the configuration value, not the configuration system. An important advantage of this separation is that it decouples Object Composition from the configuration system in use, making it possible to test without the existence of a (valid) configuration file.

本例中的Composition Root分布在两个类中,如图 4.2所示。这是意料之中的。重要的是所有类都包含在同一个模块中,在本例中,该模块是应用程序根。

The Composition Root in this example is spread out across two classes, as shown in figure 4.2. This is expected. The important thing is that all classes are contained in the same module, which, in this case, is the application root.

在此图中要注意的最重要的事情是,这两个类是整个示例应用程序中仅有的组成对象图的类。其余应用程序代码仅使用构造函数注入模式.

The most important thing to notice in this figure is that these two classes are the only classes in the entire sample application that compose object graphs. The remaining application code only uses the Constructor Injection pattern.

4.1.4 明显的依赖性爆炸

4.1.4 The apparent dependency explosion

开发人员经常听到的抱怨是,组合根导致应用程序的入口点依赖于应用程序中的所有其他程序集。在他们旧的、紧密耦合的代码库中,他们的入口点只需要依赖于直接在下面的层。这似乎是落后的,因为 DI 旨在减少所需的依赖项数量。他们认为使用 DI 会导致其应用程序入口点的依赖性激增——至少看起来是这样。

An often-heard complaint from developers is that the Composition Root causes the application’s entry point to take a dependency on all other assemblies in the application. In their old, tightly coupled code bases, their entry point only needed to depend on the layer directly below. This seems backward because DI is meant to lower the required number of dependencies. They see the use of DI as causing an explosion of dependencies in their application’s entry point — or so it seems.

这种抱怨源于开发人员误解了项目依赖项的工作方式。为了更好地了解他们担心的是什么,让我们看一下第 2 章中 Mary 应用程序的依赖图,并将其与第 3 章松散耦合应用程序的依赖图(图 4.3)进行比较。

This complaint comes from the fact that developers misunderstand how project dependencies work. To get a good view of what they’re worried about, let’s take a look at the dependency graph of Mary’s application from chapter 2 and compare that with the dependency graph of the loosely coupled application of chapter 3 (figure 4.3).

04-03.eps

图 4.3 比较 Mary 的应用程序与松耦合应用程序的依赖关系图

Figure 4.3 Comparing the dependency graph of Mary’s application to that of the loosely coupled application

乍一看,与 Mary 的应用程序“只有”三个依赖项相比,松散耦合的应用程序中似乎确实多了两个依赖项。然而,该图具有误导性。

At first glance, it indeed looks as if there are two more dependencies in the loosely coupled application, compared to Mary’s application with “only” three dependencies. The diagram, however, is misleading.

对数据访问层的更改也会波及到 UI 层,正如我们在上一章中讨论的那样,没有数据访问层就无法部署 UI 层。尽管图中没有显示,但 UI 和数据访问层之间存在依赖关系。程序集依赖项实际上是可传递的。

Changes to the data access layer also ripple through the UI layer and, as we discussed in the previous chapter, the UI layer can’t be deployed without the data access layer. Even though the diagram doesn’t show it, there’s a dependency between the UI and the data access layer. Assembly dependencies are in fact transitive.

这种传递关系意味着,因为 Mary 的 UI 依赖于域,而域依赖于数据访问,所以 UI 也依赖于数据访问,这正是您在部署应用程序时会遇到的行为。如果您查看 Mary 的应用程序中项目之间的依赖关系,您会发现一些不同的东西(图 4.4)。

This transitive relationship means that because Mary’s UI depends on the domain, and the domain depends on data access, the UI depends on data access too, which is exactly the behavior you’ll experience when deploying the application. If you take a look at the dependencies between the projects in Mary’s application, you’ll see something different (figure 4.4).

04-04.eps

图 4.4 Mary 应用程序中库之间的依赖关系

Figure 4.4 The dependencies between the libraries in Mary’s application

如您所见,即使在 Mary 的应用程序中,入口点也取决于所有库。Mary 的入口点和松耦合应用程序的组合根具有相同数量的依赖项。但是请记住,依赖性不是由模块的数量定义的,而是由每个模块依赖另一个模块的次数定义的。结果,Mary 的应用程序中所有模块之间的依赖项总数实际上是六个。这比松散耦合的应用程序多了一个。

As you can see, even in Mary’s application, the entry point depends on all libraries. Both Mary’s entry point and the Composition Root of the loosely coupled application have the same number of dependencies. Remember, though, that dependencies aren’t defined by the number of modules, but the number of times each module depends on another module. As a result, the total number of dependencies between all modules in Mary’s application is, in fact, six. That’s one more than the loosely coupled application.

现在想象一个包含数十个项目的应用程序。不难想象,与松散耦合的代码库相比,紧密耦合的代码库中的依赖项数量是如何爆炸式增长的。但是,通过编写应用组合根模式的松散耦合代码,您可以减少依赖项的数量。正如您在上一章中看到的,这使您可以用不同的模块替换完整的模块,这在紧密耦合的代码库中更难。

Now imagine an application with dozens of projects. It’s not hard to imagine how the number of dependencies in a tightly coupled code base explodes compared with a loosely coupled code base. But, by writing loosely coupled code that applies the Composition Root pattern, you can lower the number of dependencies. As you’ve seen in the previous chapter, this lets you replace complete modules with different ones, which is harder in a tightly coupled code base.

Composition Root模式适用于所有使用 DI 开发的应用程序,但只有启动项目才会有Composition Root组合根是消除消费者创建依赖关系的责任的结果。为此,您可以应用两种模式:构造函数注入财产注入. 构造函数注入是最常见的,应该几乎完全使用。因为构造函数注入是最常用的模式,所以我们接下来会讨论它。

The Composition Root pattern applies to all applications developed using DI, but only startup projects will have a Composition Root. A Composition Root is the result of removing the responsibility for the creation of Dependencies from consumers. To achieve this, you can apply two patterns: Constructor Injection and Property Injection. Constructor Injection is the most common and should be used almost exclusively. Because Constructor Injection is the most commonly used pattern, we’ll discuss that next.

4.2 构造函数注入

4.2 Constructor Injection

我们如何保证我们当前正在开发的类始终可以使用必要的Volatile Dependency ?

How do we guarantee that a necessary Volatile Dependency is always available to the class we’re currently developing?

通过要求所有调用者将Volatile Dependency作为参数提供给类的构造函数。

By requiring all callers to supply the Volatile Dependency as a parameter to the class’s constructor.

当类需要Dependency的实例时,您可以通过类的构造函数提供该Dependency,使其能够存储引用以供将来使用。

When a class requires an instance of a Dependency, you can supply that Dependency through the class’s constructor, enabling it to store the reference for future use.

构造函数签名与类型一起编译,并且可供所有人查看。它清楚地记录了该类需要通过其构造函数请求的依赖项。图 4.5演示了这一点。

The constructor signature is compiled with the type and is available for all to see. It clearly documents that the class requires the Dependencies it requests through its constructor. Figure 4.5 demonstrates this.

04-05.eps

图 4.5使用构造函数注入构造HomeController具有所需IProductService依赖关系 的实例

Figure 4.5 Constructing a HomeController instance with a required IProductServiceDependency using Constructor Injection

此图显示消费类需要依赖HomeController项的实例才能工作,因此它需要组合根(客户端)通过其构造函数提供实例。这保证了实例在需要时可用。IProductService HomeController

This figure shows that the consuming class HomeController needs an instance of the IProductService Dependency to work, so it requires the Composition Root (the client) to supply an instance via its constructor. This guarantees that the instance is available to HomeController when it’s needed.

4.2.1构造函数注入如何工作

4.2.1 How Constructor Injection works

需要依赖项的类必须公开一个公共构造函数,该构造函数将所需依赖项的实例作为构造函数参数。这应该是唯一公开可用的构造函数。如果需要多个Dependency,可以将额外的构造函数参数添加到同一个构造函数中。清单 4.5显示了HomeController类的定义4.5

The class that needs the Dependency must expose a public constructor that takes an instance of the required Dependency as a constructor argument. This should be the only publicly available constructor. If more than one Dependency is needed, additional constructor arguments can be added to the same constructor. Listing 4.5 shows the definition of the HomeController class of figure 4.5.

清单 4.5使用构造函数注入 注入依赖

Listing 4.5 Injecting a Dependency using Constructor Injection

public class HomeController
{
    private readonly IProductService service;    ①  

    public HomeController(    ②  
        IProductService service)    ③  
    {
        if (service == null)    ④  
            throw new ArgumentNullException("service");  ④  

        this.service = service;    ⑤  
    }
}

IProductService Dependency是一个必需的构造函数参数HomeController;任何不提供实例的客户端IProductService都无法编译。但是,因为接口是引用类型,调用者可以作为参数传入以使调用代码编译。您需要使用保护条款来保护班级免受此类滥用。1  因为编译器和 Guard Clause 的共同努力保证了构造函数参数在没有抛出异常的情况下有效,构造函数可以存储依赖以供将来使用,而无需了解真正的实现。null

The IProductService Dependency is a required constructor argument of HomeController; any client that doesn’t supply an instance of IProductService can’t compile. But, because an interface is a reference type, a caller can pass in null as an argument to make the calling code compile. You need to protect the class against such misuse with a Guard Clause.1  Because the combined efforts of the compiler and the Guard Clause guarantee that the constructor argument is valid if no exception is thrown, the constructor can store the Dependency for future use without knowing anything about the real implementation.

将保存依赖项的字段标记为readonly. 这保证一旦构造函数的初始化逻辑执行完毕,该字段就不能被修改。null从 DI 的角度来看,这并不是严格要求的,但它可以防止您在依赖类代码的其他地方意外修改该字段(例如将其设置为)。

It’s good practice to mark the field holding the Dependency as readonly. This guarantees that once the initialization logic of the constructor has executed, the field can’t be modified. This isn’t strictly required from a DI point of view, but it protects you from accidentally modifying the field (such as setting it to null) somewhere else in the depending class’s code.

当构造函数返回时,类的新实例与注入其中的依赖项的正确实例处于一致状态。因为构造的类持有对此Dependency的引用,所以它可以经常使用Dependency任何其他成员所必需的。它的成员不需要测试null,因为实例保证存在。

When the constructor has returned, the new instance of the class is in a consistent state with a proper instance of its Dependency injected into it. Because the constructed class holds a reference to this Dependency, it can use the Dependency as often as necessary from any of its other members. Its members don’t need to test for null, because the instance is guaranteed to be present.

4.2.2 何时使用构造函数注入

4.2.2 When to use Constructor Injection

构造函数注入应该是 DI 的默认选择。它解决了一个类需要一个或多个Dependencies并且没有合理的Local Defaults可用的最常见场景。

Constructor Injection should be your default choice for DI. It addresses the most common scenario where a class requires one or more Dependencies, and no reasonable Local Defaults are available.

构造函数注入解决了对象需要依赖但没有合理的本地默认可用的常见情况,因为它保证必须提供依赖。如果依赖类在没有Dependency的情况下绝对无法运行,那么这种保证就很有价值。表 4.1总结了构造函数注入的优点和缺点。

Constructor Injection addresses the common scenario of an object requiring a Dependency with no reasonable Local Default available, because it guarantees that the Dependency must be provided. If the depending class absolutely can’t function without the Dependency, such a guarantee is valuable. Table 4.1 provides a summary of the advantages and disadvantages of Constructor Injection.

表 4.1 构造函数注入优缺点
优点缺点
保证注入

易于实现

静态声明类的依赖项
应用约束构造反模式的框架可能会使使用构造函数注入变得困难。

在本地库可以提供良好默认实现的情况下,属性注入也可能是一个很好的选择,但通常情况并非如此。在前面的章节中,我们展示了许多将存储库作为依赖项的示例。这些是Dependencies的好例子,其中本地库不能提供好的默认实现,因为正确的实现属于专门的数据访问库。除了已经讨论过的保证注入之外,使用清单 4.5中的结构也很容易实现这种模式。

In cases where the local library can supply a good default implementation, Property Injection can also be a good fit, but this is usually not the case. In the earlier chapters, we showed many examples of Repositories as Dependencies. These are good examples of Dependencies, where the local library can supply no good default implementation because the proper implementations belong in specialized data access libraries. Apart from the guaranteed injection already discussed, this pattern is also easy to implement using the structure presented in listing 4.5.

Constructor Injection的主要缺点是,如果您正在构建的类被您当前的应用程序框架调用,您可能需要自定义该框架以支持它。某些框架,尤其是较旧的框架,假定您的类将具有无参数构造函数。3   (这称为约束构造反模式,我们将在下一章中更详细地讨论这个问题。)在这种情况下,当无参数构造函数不可用时,框架将需要特殊帮助来创建实例。在第 7 章中,我们将解释如何为常见的应用程序框架启用构造函数注入。

The main disadvantage to Constructor Injection is that if the class you’re building is called by your current application framework, you might need to customize that framework to support it. Some frameworks, especially older ones, assume that your classes will have a parameterless constructor.3  (This is called the Constrained Construction anti-pattern, and we’ll discuss this in more detail in the next chapter.) In this case, the framework will need special help creating instances when a parameterless constructor isn’t available. In chapter 7, we’ll explain how to enable Constructor Injection for common application frameworks.

正如之前在 4.1 节中讨论的那样,构造函数注入的一个明显缺点是它需要立即初始化整个依赖图。虽然这听起来效率低下,但这很少成为问题。毕竟,即使对于复杂的对象图,我们通常也是在谈论创建几十个新的对象实例,而创建对象实例是 .NET Framework 执行速度非常快的事情。您的应用程序可能存在的任何性能瓶颈都会出现在其他地方,所以不用担心。4个 

As previously discussed in section 4.1, an apparent disadvantage of Constructor Injection is that it requires that the entire Dependency graph be initialized immediately. Although this sounds inefficient, it’s rarely an issue. After all, even for a complex object graph, we’re typically talking about creating a few dozen new object instances, and creating an object instance is something the .NET Framework does extremely fast. Any performance bottleneck your application may have will appear in other places, so don’t worry about it.4 

现在您知道构造函数注入是应用 DI 的首选方式,让我们看一些已知的示例。为此,我们接下来将讨论.NET BCL 中的构造函数注入

Now that you know that Constructor Injection is the preferred way of applying DI, let’s take a look at some known examples. For this, we’ll discuss Constructor Injection in the .NET BCL next.

4.2.3构造函数注入的已知用法

4.2.3 Known use of Constructor Injection

尽管构造函数注入在使用 DI 的应用程序中普遍存在,但它在 BCL 中并不常见。这主要是因为 BCL 是一组可重用的库,而不是一个成熟的应用程序。您可以在 BCL 中看到某种构造函数注入的两个相关示例是和类System.IO.StreamReaderSystem.IO.StreamWriter. 两者都System.IO.Stream在其构造函数中获取一个实例。这是所有与StreamWritersStream相关的构造函数;StreamReader构造函数是相似的:

Although Constructor Injection tends to be ubiquitous in applications employing DI, it isn’t very present in the BCL. This is mainly because the BCL is a set of reusable libraries and not a full-fledged application. Two related examples where you can see a sort of Constructor Injection in the BCL is with the System.IO.StreamReader and System.IO.StreamWriter classes. Both take a System.IO.Stream instance in their constructors. Here’s all of StreamWriter's Stream-related constructors; the StreamReader constructors are similar:

public StreamWriter(Stream stream);
public StreamWriter(Stream stream, Encoding encoding);
public StreamWriter(Stream stream, Encoding encoding, int bufferSize);

Stream是一个抽象类,作为抽象类,并其上StreamWriter运行StreamReader以履行其职责。你可以提供任何Stream 在他们的构造函数中实现,他们会使用它,但ArgumentNullExceptions如果你试图给他们一个null流,他们会抛出。

Stream is an abstract class that serves as an Abstraction on which StreamWriter and StreamReader operate to perform their duties. You can supply any Stream implementation in their constructors, and they’ll use it, but they’ll throw ArgumentNullExceptions if you try to slip them a null stream.

尽管 BCL 提供了示例,您可以在其中看到正在使用的构造函数注入,但查看工作示例总是更有指导意义。下一节将带您完成一个完整的实施示例。

Although the BCL provides examples where you can see Constructor Injection in use, it’s always more instructive to see a working example. The next section walks you through a full implementation example.

4.2.4 示例:向特色产品添加货币换算

4.2.4 Example: Adding currency conversions to the featured products

玛丽的老板说她的应用程序运行良好,但现在一些使用它的客户想要使用不同的货币支付商品。她能否编写一些新代码,使该应用程序能够以不同货币显示和计算成本?Mary 叹了口气,意识到硬编码几种不同的货币换算是不够的。随着时间的推移,她需要编写足够灵活的代码来适应任何货币。DI 又来电了。

Mary’s boss says her app is working fine, but now some customers who are using it want to pay for goods in different currencies. Can she write some new code that enables the app to display and calculate costs in different currencies? Mary sighs and realizes that it’s not going to be enough to hard-code in a few different currency conversions. She’ll need to write code flexible enough to accommodate any currency over time. DI is calling again.

Mary 需要的既是表示货币及其货币的对象,又是允许将货币从一种货币转换为另一种货币的抽象。她会给抽象 命名。为简单起见, the将只有一个 currency ,并且由 a和 an组成,如图 4.6所示。ICurrencyConverterCurrencyCodeMoneyCurrencyAmount

What Mary needs is both an object for representing money and its currency and an Abstraction that allows converting money from one currency into another. She’ll name the Abstraction ICurrencyConverter. For simplicity, the Currency will only have a currency Code, and Money is composed of both a Currency and an Amount, as shown in figure 4.6.

04-06.eps

图 4.6 兑换货币使用ICurrencyConverter

Figure 4.6 Exchanging currencies using ICurrencyConverter

下面的清单显示了类和Currency接口,如图 4.6 所示MoneyICurrencyConverter

The following listing shows the Currency and Money classes, and the ICurrencyConverter interface, as envisioned in figure 4.6.

清单4.6Currency和界面MoneyICurrencyConverter

Listing 4.6 Currency, Money, and the ICurrencyConverter interface

public interface ICurrencyConverter
{
    Money Exchange(Money money, Currency targetCurrency);
}

public class Currency
{
    public readonly string Code;

    public Currency(string code)
    {
        if (code == null) throw new ArgumentNullException("code");

        this.Code = code;
    }
}

public class Money
{
    public readonly decimal Amount;
    public readonly Currency Currency;

    public Money(decimal amount, Currency currency)
    {
        if (currency == null) throw new ArgumentNullException("currency");

        this.Amount = amount;
        this.Currency = currency;
    }
}

可能表示进程外资源,例如 Web 服务或提供转换率的数据库。这意味着在单独的项目(例如数据访问层)中实现具体的内容是合适的。因此,没有合理的本地默认值ICurrencyConverterICurrencyConverter.

An ICurrencyConverter is likely to represent an out-of-process resource, such as a web service or a database that supplies conversion rates. This means that it’d be fitting to implement a concrete ICurrencyConverter in a separate project, such as a data access layer. Hence, there’s no reasonable Local Default.

同时,ProductService班级将需要一个ICurrencyConverter. 构造函数注入非常适合。以下清单显示了如何将ICurrencyConverter 依赖项注入到ProductService.

At the same time, the ProductService class will need an ICurrencyConverter. Constructor Injection is a good fit. The following listing shows how the ICurrencyConverter Dependency is injected into ProductService.

清单 4.7 注入ICurrencyConverter一个ProductService

Listing 4.7 Injecting an ICurrencyConverter into ProductService

public class ProductService : IProductService
{
    private readonly IProductRepository repository;
    private readonly IUserContext userContext;
    private readonly ICurrencyConverter converter;

    public ProductService(
        IProductRepository repository,
        IUserContext userContext,
        ICurrencyConverter converter)
    {
        if (repository == null)
            throw new ArgumentNullException("repository");
        if (userContext == null)
            throw new ArgumentNullException("userContext");
        if (converter == null)
            throw new ArgumentNullException("converter");

        this.repository = repository;
        this.userContext = userContext;
        this.converter = converter;
    }
}

因为ProductService已经有了对 and 的依赖,我们添加新的依赖作为第三个构造函数参数,然后按照清单 4.5中列出的相同顺序进行操作。警卫IProductRepositoryIUserContextICurrencyConverter Clauses 保证Dependencies不为 null,这意味着将它们存储起来以供以后在只读字段中使用是安全的。因为 anICurrencyConverter保证存在于 中ProductService,所以它可以在任何地方使用;例如,在GetFeaturedProducts方法中如此处所示。

Because the ProductService class already had a Dependency on IProductRepository and IUserContext, we add the new ICurrencyConverter Dependency as a third constructor argument and then follow the same sequence outlined in listing 4.5. Guard Clauses guarantee that the Dependencies aren’t null, which means it’s safe to store them for later use in read-only fields. Because an ICurrencyConverter is guaranteed to be present in ProductService, it can be used from anywhere; for example, in the GetFeaturedProducts method as shown here.

清单 4.8 ProductService使用ICurrencyConverter

Listing 4.8 ProductService using ICurrencyConverter

public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
    Currency userCurrency = this.userContext.Currency;    ①  

    var products =
        this.repository.GetFeaturedProducts();

    return
        from product in products
        let unitPrice = product.UnitPrice    ②  
        let amount = this.converter.Exchange(    ③  
            money: unitPrice,    ③  
            targetCurrency: userCurrency)    ③  
        select product
            .WithUnitPrice(amount)
            .ApplyDiscountFor(this.userContext);
}

请注意,您可以使用该converter字段无需提前检查其可用性。那是因为它保证存在。

Notice that you can use the converter field without needing to check its availability in advance. That’s because it’s guaranteed to be present.

4.2.5 总结

4.2.5 Wrap-up

构造函数注入是可用的最普遍适用的 DI 模式,也是最容易正确实现的模式。它适用于需要依赖项时。如果您需要使依赖项成为可选的,您可以更改为Property Injection如果它有一个合适的Local Default

Constructor Injection is the most generally applicable DI pattern available, and also the easiest to implement correctly. It applies when the Dependency is required. If you need to make the Dependency optional, you can change to Property Injection if it has a proper Local Default.

本章的下一个模式是方法注入,它采用的方法略有不同。它往往更适用于您已经拥有要传递给调用的协作者的依赖项的情况。

The next pattern in this chapter is Method Injection, which takes a slightly different approach. It tends to apply more to the situation where you already have a Dependency that you want to pass on to the collaborators you invoke.

4.3 方法注入

4.3 Method Injection

当每个操作都不同时,我们如何将依赖项注入到类中?

How can we inject a Dependency into a class when it’s different for each operation?

通过将其作为方法参数提供。

By supplying it as a method parameter.

Dependency可以随每个方法调用而变化的情况下,或者这种Dependency的消费者可以在每次调用时变化的情况下,您可以通过方法参数提供Dependency 。

In cases where a Dependency can vary with each method call, or the consumer of such a Dependency can vary on each call, you can supply a Dependency via a method parameter.

04-09.eps

图 4.7 使用方法注入,在每个方法调用中ProductService创建一个实例Product并将实例注入IUserContext到其中。Product.ApplyDiscountFor

Figure 4.7 Using Method Injection, ProductService creates an instance of Product and injects an instance of IUserContext into Product.ApplyDiscountFor with each method call.

4.3.1方法注入如何工作

4.3.1 How Method Injection works

调用者在每个方法调用中提供依赖项作为方法参数。Mary 的电子商务应用程序中的这种方法的一个示例是在Product类中,其中ApplyDiscountFor方法使用方法注入接受IUserContext 依赖项:

The caller supplies the Dependency as a method parameter in each method call. An example of this approach in Mary’s e-commerce application is in the Product class, where the ApplyDiscountFor method accepts an IUserContext Dependency using Method Injection:

04-12_hedgehog.eps

IUserContext提供要运行的操作的上下文信息,这是方法注入的常见场景。通常这个上下文将与一个“适当的”值一起提供给一个方法,如清单 4.9所示。

IUserContext presents contextual information for the operation to run, which is a common scenario for Method Injection. Often this context will be supplied to a method alongside a “proper” value, as shown in listing 4.9.

清单 4.9将依赖项与适当的值一起 传递

Listing 4.9 Passing a Dependency alongside a proper value

public decimal CalculateDiscountPrice(decimal price, IUserContext context)
{
    if (context == null) throw new ArgumentNullException("context");

    decimal discount = context.IsInRole(Role.PreferredCustomer) ? .95m : 1;

    return price * discount;
}

price参数表示该方法应该操作的值,而context包含有关操作的当前上下文的信息;在这种情况下,有关当前用户的信息。调用者向方法提供依赖项。正如您之前多次看到的那样,Guard Clause 保证上下文对方法体的其余部分可用。

The price value parameter represents the value on which the method is supposed to operate, whereas context contains information about the current context of the operation; in this case, information about the current user. The caller supplies the Dependency to the method. As you’ve seen many times before, the Guard Clause guarantees that the context is available to the rest of the method body.

4.3.2 何时使用方法注入

4.3.2 When to use Method Injection

方法注入不同于其他类型的 DI 模式,因为注入不是在组合根中发生,而是在调用时动态发生。这允许调用者提供特定于操作的上下文,这是 .NET BCL 中使用的常见扩展机制。表 4.2总结了方法注入的优点和缺点。

Method Injection is different from other types of DI patterns in that the injection doesn’t happen in a Composition Root but, rather, dynamically at invocation. This allows the caller to provide an operation-specific context, which is a common extensibility mechanism used in the .NET BCL. Table 4.2 provides a summary of the advantages and disadvantages of Method Injection.

表 4.2 方法注入的优缺点
优点缺点


允许调用者提供特定于操作上下文
有限

的适用性导致依赖成为类或其抽象的公共 API 的一部分

应用方法注入有两个典型的用例:

There are two typical use cases for applying Method Injection:

  • 当注入的依赖项的消费者在每次调用时发生变化
  • When the consumer of the injected Dependency varies on each call
  • 当注入的依赖项在每次调用消费者时发生变化
  • When the injected Dependency varies on each call to a consumer

以下部分显示了每个示例。清单 4.9是消费者如何变化的一个例子。这是最常见的形式,因此我们将从提供另一个示例开始。

The following sections show an example of each. Listing 4.9 is an example of how the consumer varies. This is the most common form, which is why we’ll start with providing another example.

示例:在每次方法调用时改变Dependency的使用者

Example: Varying the Dependency's consumer on each method call

当你练习领域驱动设计时(DDD),通常创建包含域逻辑的域实体,有效地将运行时数据与同一类中的行为混合。8  然而,实体通常不是在组合根中创建的。以下面的Customer Entity为例。

When you practice Domain-Driven Design (DDD), it’s common to create domain Entities that contain domain logic, effectively mixing runtime data with behavior in the same class.8  Entities, however, are typically not created within the Composition Root. Take the following Customer Entity, for example.

清单 4.10包含域逻辑但没有依赖项 的实体(尚未)

Listing 4.10 An Entity containing domain logic but no Dependencies (yet)

public class Customer    ①  
{
    public Guid Id { get; private set; }    ②  
    public string Name { get; private set; }    ②  

    public Customer(Guid id, string name)    ③  
    {
        ...
    }

    public void RedeemVoucher(Voucher voucher) ...    ④  


    public void MakePreferred() ...    ⑤  
}

清单 4.10中的和方法是域方法。实现让客户兑换优惠券的领域逻辑。(您购买本书时可能已经兑换了优惠券以获得折扣。)是该方法使用的值对象9   。,另一方面,实现促进客户的领域逻辑。普通客户可以升级为首选客户,这可能会提供某些优势和折扣,类似于常旅客航空公司的客户。RedeemVoucherMakePreferredRedeemVouchervoucherMakePreferred

The RedeemVoucher and MakePreferred methods in listing 4.10 are domain methods. RedeemVoucher implements the domain logic that lets the customer redeem a voucher. (You may have redeemed a voucher to get a discount when you purchased this book.) voucher is a value object9  used by the method. MakePreferred, on the other hand, implements the domain logic that promotes the customer. A regular customer could get upgraded to become a preferred customer, which might give certain advantages and discounts, similar to being a frequent flyer airline customer.

除了通常的数据成员集之外还包含行为的实体将很容易获得范围广泛的方法,每个方法都需要自己的Dependencies。尽管您可能想使用构造函数注入来注入此类Dependencies,但这会导致需要创建每个此类Entity及其所有Dependencies的情况,即使对于给定的用例可能只需要几个。这使测试Entity的逻辑变得复杂,因为所有Dependencies都需要提供给构造函数,即使测试可能只对少数Dependencies感兴趣。方法注入,如下一个清单所示,提供了一个更好的选择。

Entities that contain behavior besides their usual set of data members would easily get a wide range of methods, each requiring their own Dependencies. Although you might be tempted to use Constructor Injection to inject such Dependencies, that leads to a situation where each such Entity needs to be created with all of its Dependencies, even though only a few may be necessary for a given use case. This complicates testing the logic of an Entity, because all Dependencies need to be supplied to the constructor, even though a test might only be interested in a few Dependencies. Method Injection, as shown in the next listing, offers a better alternative.

清单 4.11使用方法注入实体

Listing 4.11 An Entity using Method Injection

public class Customer
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }

    public Customer(Guid id, string name)
    {
        ...
    }

    public void RedeemVoucher(    ①  
        Voucher voucher,
        IVoucherRedemptionService service)
    {
        if (voucher == null)
            throw new ArgumentNullException("voucher");
        if (service == null)
            throw new ArgumentNullException("service");

        service.ApplyRedemptionForCustomer(
            voucher,
            this.Id);
    }

    public void MakePreferred(IEventHandler handler)    ①  
    {
        if (handler == null)
            throw new ArgumentNullException("handler");

        handler.Publish(new CustomerMadePreferred(this.Id));
    }
}

CustomerServices组件内部,CustomerRedeemVoucher方法可以在通过调用传递依赖项时调用,如下所示。IVoucherRedemptionService

Inside a CustomerServices component, the Customer's RedeemVoucher method can be called while passing the IVoucherRedemptionService Dependency with the call, as shown next.

清单 4.12 使用方法注入传递依赖项的组件

Listing 4.12 A component using Method Injection to pass a Dependency

public class CustomerServices : ICustomerServices
{
    private readonly ICustomerRepository repository;
    private readonly IVoucherRedemptionService service;

    public CustomerServices(    ①  
        ICustomerRepository repository,    ①  
        IVoucherRedemptionService service)    ①  
    {
        this.repository = repository;
        this.service = service;
    }

    public void RedeemVoucher(
        Guid customerId, Voucher voucher)
    {
        var customer =
            this.repository.GetById(customerId);

        customer.RedeemVoucher(voucher, this.service);    ②  

        this.repository.Save(customer);
    }
}

清单 4.12中,只请求了一个Customer实例。但是可以使用大量客户和优惠券一遍又一遍地调用单个实例,从而导致将相同的内容提供给许多不同的实例。是Dependency的消费者,当您重用Dependency时,您正在改变消费者。ICustomerRepositoryCustomerServicesIVoucherRedemptionServiceCustomerCustomerIVoucherRedemptionService

In listing 4.12, only a single Customer instance is requested from ICustomerRepository. But a single CustomerServices instance can be called over and over again using a multitude of customers and vouchers, causing the same IVoucherRedemptionService to be supplied to many different Customer instances. Customer is the consumer of the IVoucherRedemptionService Dependency and, while you’re reusing the Dependency, you’re varying the consumer.

这类似于清单 4.9中显示的第一个方法注入示例和方法ApplyDiscountFor在清单 3.8 中讨论。相反的情况是,当您改变依赖关系同时保持其消费者在附近时。

This is similar to the first Method Injection example shown in listing 4.9 and the ApplyDiscountFor method discussed in listing 3.8. The opposite case is when you vary the Dependency while keeping its consumers around.

示例:在每个方法调用上改变注入的依赖项

Example: Varying the injected Dependency on each method call

想象一个用于图形绘图应用程序的插件系统,您希望每个人都能够在其中插入自己的图像效果。外部图像效果可能需要有关运行时上下文的信息,这些信息可以由应用程序传递给图像效果。这是应用方法注入的典型用例。您可以定义以下接口来应用这些效果:

Imagine an add-in system for a graphical drawing application, where you want everyone to be able to plug in their own image effects. External image effects might require information about the runtime context, which can be passed on by the application to the image effect. This is a typical use case for applying Method Injection. You can define the following interface for applying those effects:

public interface IImageEffectAddIn    ①  
{
    Bitmap Apply(    ②  
        Bitmap source,
        IApplicationContext context);    ③  
}

IImageEffectAddIn依赖可以随每次调用方法而变化IApplicationContext Apply,为效果提供有关调用操作的上下文的信息。任何实现此接口的类都可以用作插件。一些实现可能根本不关心context,而其他实现会。

The IImageEffectAddIn's IApplicationContext Dependency can vary with each call to the Apply method, providing the effect with information about the context in which the operation is being invoked. Any class implementing this interface can be used as an add-in. Some implementations may not care about the context at all, whereas other implementations will.

客户端可以通过使用源和上下文调用每个加载项来使用加载项列表以返回聚合结果,如下一个清单所示。Bitmap

A client can use a list of add-ins by calling each with a source Bitmap and a context to return an aggregated result, as shown in the next listing.

清单 4.13 示例插件客户端

Listing 4.13 A sample add-in client

public Bitmap ApplyEffects(Bitmap source)
{
    if (source == null) throw new ArgumentNullException("source");

    Bitmap result = source;

    foreach (IImageEffectAddIn effect in this.effects)
    {
        result = effect.Apply(result, this.context);
    }

    return result;
}

私有effects字段是一个实例列表,它允许客户端循环遍历列表以调用每个加载项的方法IImageEffectAddInApply. 每次Apply在加载项上调用该方法时,由context字段表示的操作上下文作为方法参数传递:

The private effects field is a list of IImageEffectAddIn instances, which allows the client to loop through the list to invoke each add-in’s Apply method. Each time the Apply method is invoked on an add-in, the operation’s context, represented by the context field, is passed as a method parameter:

result = effect.Apply(result, this.context);

有时,价值和操作上下文被封装在一个抽象中,作为两者的组合。需要注意的重要一点是:正如您在两个示例中所见,通过方法注入注入的依赖成为抽象定义的一部分。如果Dependency包含由其直接调用者提供的运行时信息,这通常是可取的。

At times, the value and the operational context are encapsulated in a single Abstraction that works as a combination of both. An important thing to note is this: as you’ve seen in both examples, the Dependency injected via Method Injection becomes part of the definition of the Abstraction. This is typically desirable in case that Dependency contains runtime information that’s supplied by its direct callers.

依赖是调用者的实现细节的情况下,您应该尝试防止抽象被“污染”;因此,构造函数注入是一个更好的选择。否则,您很容易最终将依赖项从应用程序对象图的顶部一直向下传递,从而导致彻底的更改。

In cases where the Dependency is an implementation detail to the caller, you should try to prevent the Abstraction from being “polluted”; therefore, Constructor Injection is a better pick. Otherwise, you could easily end up passing the Dependency from the top of our application’s object graph all the way down, causing sweeping changes.

前面的例子都展示了在Composition Root之外使用Method Injection。这是故意的。方法注入不适合在Composition Root中使用。在Composition Root中,方法注入可以使用其Dependencies初始化先前构造的类。然而,这样做会导致时间耦合,因此非常不鼓励这样做。

The previous examples all showed the use of Method Injection outside of the Composition Root. This is deliberate. Method Injection is unsuitable when used within the Composition Root. Within a Composition Root, Method Injection can initialize a previously constructed class with its Dependencies. Doing so, however, leads to Temporal Coupling and for that reason it’s highly discouraged.

时间耦合代码的味道

The Temporal Coupling code smell

时间耦合是 API 设计中的一个常见问题。当一个类的两个或多个成员之间存在隐式关系时,就会发生这种情况,要求客户先调用一个成员再调用另一个。这在时间维度上紧密耦合了成员。原型示例是使用Initialize方法,尽管可以找到大量其他示例——甚至在 BCL 中也是如此。例如,这种用法编译但在运行时失败:System.ServiceModel.EndpointAddressBuilder

Temporal Coupling is a common problem in API design. It occurs when there’s an implicit relationship between two or more members of a class, requiring clients to invoke one member before the other. This tightly couples the members in the temporal dimension. The archetypical example is the use of an Initialize method, although copious other examples can be found — even in the BCL. As an example, this usage of System.ServiceModel.EndpointAddressBuilder compiles but fails at runtime:

var builder = new EndpointAddressBuilder();
var address = builder.ToEndpointAddress();

事实证明,在创建 之前需要一个 URI。以下代码在运行时编译并成功:EndpointAddress

It turns out that an URI is required before an EndpointAddress can be created. The following code compiles and succeeds at runtime:

var builder = new EndpointAddressBuilder();
builder.Uri = new UriBuilder().Uri;
var address = builder.ToEndpointAddress();

API 没有暗示这是必要的,但属性之间存在时间耦合UriToEndpointAddress方法.

The API provides no hint that this is necessary, but there’s a Temporal Coupling between the Uri property and the ToEndpointAddress method.

当在Composition Root内部应用时,重复出现的模式是使用某种Initialize方法,如代码清单 4.14所示。

When applied inside the Composition Root, a recurring pattern is the use of some Initialize method, as shown in listing 4.14.

坏.tif

清单 4.14 时间耦合示例

Listing 4.14 Temporal Coupling example

public class Component
{
    private ISomeInterface dependency;

    public void Initialize(    ①  
        ISomeInterface dependency)    ①  
    {
        this.dependency = dependency;
    }

    public void DoSomething()
    {
        if (this.dependency == null)    ②  
            throw new InvalidOperationException(    ②  
                "Call Initialize first.");    ②  

        this.dependency.DoStuff();
    }
}

在语义上,方法的名称是一个线索,但在结构层面上,这个 API 没有给我们任何Temporal CouplingInitialize的迹象。因此,像这样的代码可以编译,但会在运行时抛出异常:

Semantically, the name of the Initialize method is a clue, but on a structural level, this API gives us no indication of Temporal Coupling. Thus, code like this compiles, but throws an exception at runtime:

var c = new Component();
c.DoSomething();

这个问题的解决方案现在应该很明显了——你应该应用构造函数注入来代替:

The solution to this problem should be obvious by now — you should apply Constructor Injection instead:

public class Component
{
    private readonly ISomeInterface dependency;

    public Component(ISomeInterface dependency)
    {
        if (dependency == null)
            throw new ArgumentNullException("dependency");

        this.dependency = dependency;
    }

    public void DoSomething()
    {
        this.dependency.DoStuff();
    }
}

4.3.3 方法注入的已知用法

4.3.3 Known use of Method Injection

.NET BCL 提供了许多方法注入示例,尤其是在System.ComponentModel命名空间中。您用于为组件实现自定义设计时功能。它有一个方法System.ComponentModel.Design.IDesignerInitialize它需要一个IComponent实例,以便它知道它当前正在帮助设计哪个组件。(请注意,此Initialize方法会导致时间耦合。)设计器是由IDesignerHost实现创建的,这些实现也将IComponent实例作为参数来创建设计器:

The .NET BCL provides many examples of Method Injection, particularly in the System.ComponentModel namespace. You use System.ComponentModel.Design.IDesigner for implementing custom design-time functionality for components. It has an Initialize method that takes an IComponent instance so that it knows which component it’s currently helping to design. (Note that this Initialize method causes Temporal Coupling.) Designers are created by IDesignerHost implementations that also take IComponent instances as parameters to create designers:

IDesigner GetDesigner(IComponent component);

这是参数本身携带信息的场景的一个很好的例子。component可以携带有关创建哪些信息的信息IDesigner,但同时,它也是设计人员随后必须对其进行操作的组件。

This is a good example of a scenario where the parameter itself carries information. The component can carry information about which IDesigner to create, but at the same time, it’s also the component on which the designer must subsequently operate.

System.ComponentModel命名空间中的另一个例子TypeConverter班级提供。顾名思义,它的几个方法采用了它的一个实例,传递有关当前操作上下文的信息,例如有关类型属性的信息。因为这样的方法很多,我们不想一一列举,这里举一个有代表性的例子:ITypeDescriptorContext

Another example in the System.ComponentModel namespace is provided by the TypeConverter class. Several of its methods take an instance of ITypeDescriptorContext that, as the name says, conveys information about the context of the current operation, such as information about the type’s properties. Because there are many such methods, we don’t want to list them all, but here’s a representative example:

public virtual object ConvertTo(ITypeDescriptorContext context,
    CultureInfo culture, object value, Type destinationType)

在此方法中,操作的上下文由context参数显式传达,而要转换的值和目标类型作为单独的参数发送。context实施者可以在他们认为合适的时候使用或忽略参数。

In this method, the context of the operation is communicated explicitly by the context parameter, whereas the value to be converted and the destination type are sent as separate parameters. Implementers can use or ignore the context parameter as they see fit.

ASP.NET Core MVC 还包含几个方法注入示例。你可以使用IValidationAttributeAdapterProvider界面,例如,提供实例。它唯一的方法是这样的:IAttributeAdapter

ASP.NET Core MVC also contains several examples of Method Injection. You can use the IValidationAttributeAdapterProvider interface, for instance, to provide IAttributeAdapter instances. Its only method is this:

IAttributeAdapter GetAttributeAdapter(
    ValidationAttribute attribute, IStringLocalizer stringLocalizer)

ASP.NET Core 允许将视图模型的属性标记为. 这是一种应用元数据的便捷方式,元数据描述封装在视图模型中的属性的有效性。ValidationAttribute

ASP.NET Core allows properties of view models to be marked with ValidationAttribute. It’s a convenient way to apply metadata that describes the validity of properties encapsulated in the view model.

该方法基于一个,允许返回一个 ,允许在网页中显示相关的错误信息。方法中,参数ValidationAttributeGetAttributeAdapterIAttributeAdapterGetAttributeAdapterattributeIAttributeAdapter应该为其创建的对象,而stringLocalizer是通过方法注入传递的依赖项。

Based on a ValidationAttribute, the GetAttributeAdapter method allows an IAttributeAdapter to be returned, which allows relevant error messages to be displayed in a web page. In the GetAttributeAdapter method, the attribute parameter is the object an IAttributeAdapter should be created for, whereas the stringLocalizer is the Dependency that’s passed through Method Injection.

接下来,我们将看到 Mary 如何使用方法注入来防止代码重复。当我们最后一次见到 Mary(在 4.2 节)时,她正在做:她使用构造函数注入将它注入到类中。ICurrencyConverterProductService

Next, we’ll see how Mary uses Method Injection in order to prevent code repetition. When we last saw Mary (in section 4.2), she was working on ICurrencyConverter: she injected it using Constructor Injection into the ProductService class.

4.3.4 示例:向Product实体添加货币换算

4.3.4 Example: Adding currency conversions to the ProductEntity

清单 4.8展示了该GetFeaturedProducts方法是如何调用了ICurrencyConverter.Exchange方法在 Mary 的应用程序中使用产品UnitPrice和用户的首选货币。又是这个GetFeaturedProducts方法:

Listing 4.8 showed how the GetFeaturedProducts method called the ICurrencyConverter.Exchange method using the product’s UnitPrice and the user’s preferred currency in Mary’s application. Here’s that GetFeaturedProducts method again:

public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
    Currency currency = this.userContext.Currency;

    return
        from product in this.repository.GetFeaturedProducts()
        let amount = this.converter.Exchange(product.UnitPrice, currency)
        select product
            .WithUnitPrice(amount)
            .ApplyDiscountFor(this.userContext);
}

在她的应用程序的许多部分中,将Product 实体从一个实体转换为另一个实体将是一项重复出现的任务。Currency出于这个原因,Mary 喜欢将有关转换的逻辑Product移出ProductService并将其集中为Product Entity的一部分。这可以防止系统的其他部分重复此代码。事实证明,方法注入是一个很好的选择。Mary 在 中创建了一个新ConvertTo方法Product,如下一个清单所示。

Conversions of Product Entities from one Currency to another will be a recurring task in many parts of her application. For this reason, Mary likes to move the logic concerning the conversion of the Product out of ProductService and centralize it as part of the Product Entity. This prevents other parts of the system from repeating this code. Method Injection turns out to be a great candidate for this. Mary creates a new ConvertTo method in Product, as shown in the next listing.

清单 4.15 带有方法的Product实体ConvertTo

Listing 4.15 ProductEntity with ConvertTo method

public class Product
{
    public string Name { get; set; }
    public Money UnitPrice { get; set; }
    public bool IsFeatured { get; set; }

    public Product ConvertTo(
        Currency currency,    ①  
        ICurrencyConverter converter)    ②  
    {
        if (currency == null)
            throw new ArgumentNullException("currency");
        if (converter == null)
            throw new ArgumentNullException("converter");

        var newUnitPrice =
            converter.Exchange(    ③  
                this.UnitPrice,
                currency);

        return this.WithUnitPrice(newUnitPrice);    ④  
    }

    public Product WithUnitPrice(Money unitPrice)
    {
        return new Product
        {
            Name = this.Name,
            UnitPrice = unitPrice,
            IsFeatured = this.IsFeatured
        };
    }
    ...
}

使用新ConvertTo方法,Mary 重构了GetFeaturedProducts方法.

With the new ConvertTo method, Mary refactors the GetFeaturedProducts method.

清单 4.16 GetFeaturedProducts使用ConvertTo方法

Listing 4.16 GetFeaturedProducts using ConvertTo method

public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
    Currency currency = this.userContext.Currency;

    return
        from product in this.repository.GetFeaturedProducts()
        select product
            .ConvertTo(currency, this.converter)    ①  
            .ApplyDiscountFor(this.userContext);
}

而不是调用ICurrencyConverter.Exchange方法,如您之前所见,现在传递给使用方法注入的方法。当 Mary 需要在她的代码库中的其他地方转换产品时,这简化了方法并防止了任何代码重复。通过使用方法注入而不是构造函数注入,她避免了必须构建具有所有依赖项的实体。这简化了构造和测试。GetFeaturedProductsICurrencyConverterConvertToGetFeaturedProductsProduct

Instead of calling the ICurrencyConverter.Exchange method, as you’ve seen previously, GetFeaturedProducts now passes ICurrencyConverter on to the ConvertTo method using Method Injection. This simplifies the GetFeaturedProducts method and prevents any code duplication when Mary needs to convert products elsewhere in her code base. By using Method Injection instead of Constructor Injection, she avoided having to build up the Product Entity with all of its Dependencies. This simplifies construction and testing.

与本章中的其他 DI 模式不同,当您想要向已经存在的消费者提供依赖项时,您主要使用方法注入。另一方面,使用Constructor InjectionProperty Injection ,您可以在消费者创建时向其提供依赖项。

Unlike the other DI patterns in this chapter, you mainly use Method Injection when you want to supply Dependencies to an already existing consumer. With Constructor Injection and Property Injection, on the other hand, you supply Dependencies to a consumer while it’s being created.

本章的最后一个模式是属性注入,它允许您覆盖类的本地默认值方法注入仅在组合根外部应用,属性注入,就像构造函数注入一样,是从组合根内部应用的。

The last pattern in this chapter is Property Injection, which allows you to override a class’s Local Default. Where Method Injection was solely applied outside the Composition Root, Property Injection, just as Constructor Injection, is applied from within the Composition Root.

4.4 属性注入

4.4 Property Injection

当我们有一个好的Local Default时,我们如何在类中启用 DI 作为一个选项?

How do we enable DI as an option in a class when we have a good Local Default?

通过公开一个可写属性,让调用者在想要覆盖默认行为时提供依赖项。

By exposing a writable property that lets callers supply a Dependency if they want to override the default behavior.

当一个类具有良好的Local Default,但您仍希望将其保持开放以实现可扩展性时,您可以公开一个可写属性,该属性允许客户端提供与默认类的依赖项不同的实现。如图4.8所示,想要按原样使用Consumer该类的客户可以创建该类的实例并使用它而无需再考虑,而想要修改该类行为的客户可以通过将Dependency属性设置为的不同实现IDependency

When a class has a good Local Default, but you still want to leave it open for extensibility, you can expose a writable property that allows a client to supply a different implementation of the class’s Dependency than the default. As figure 4.8 shows, clients wanting to use the Consumer class as is can create an instance of the class and use it without giving it a second thought, whereas clients wanting to modify the behavior of the class can do so by setting the Dependency property to a different implementation of IDependency.

04-10.eps

图 4.8 属性注入

Figure 4.8 Property Injection

4.4.1属性注入如何工作

4.4.1 How Property Injection works

使用Dependency的类必须公开Dependency类型的公共可写属性。在基本实现中,这可以像下面的清单一样简单。

The class that uses the Dependency must expose a public writable property of the Dependency’s type. In a bare-bones implementation, this can be as simple as the following listing.

清单 4.17 属性注入

Listing 4.17 Property Injection

public class Consumer
{
    public IDependency Dependency { get; set; }
}

Consumer取决于IDependency. IDependency客户可以通过设置Dependency属性来提供实现.

Consumer depends on IDependency. Clients can supply implementations of IDependency by setting the Dependency property.

依赖类的其他成员可以使用注入的依赖来执行它们的职责,如下所示:

Other members of the depending class can use the injected Dependency to perform their duties, like this:

public void DoSomething()
{
    this.Dependency.DoStuff();
}

不幸的是,这样的实现是脆弱的。那是因为该Dependency属性不能保证返回 的实例IDependency。如果属性的值为:NullReferenceExceptionDependencynull

Unfortunately, such an implementation is fragile. That’s because the Dependency property isn’t guaranteed to return an instance of IDependency. Code like this would throw a NullReferenceException if the value of the Dependency property is null:

var instance = new Consumer();

instance.DoSomething();    ①  

这个问题可以通过让构造函数在属性上设置默认实例,并在属性的 setter 中结合适当的 Guard Clause 来解决。如果客户端在类的生命周期中间切换依赖关系,则会出现另一个复杂情况:

This issue can be solved by letting the constructor set a default instance on the property, combined with a proper Guard Clause in the property’s setter. Another complication arises if clients switch the Dependency in the middle of the class’s lifetime:

var instance = new Consumer();

instance.Dependency = new SomeImplementation();    ①  

instance.DoSomething();

instance.Dependency = new SomeOtherImplementation();    ②  

instance.DoSomething();

这可以通过引入一个只允许客户端在初始化期间设置依赖项的内部标志来解决。11 

This can be addressed by introducing an internal flag that only allows a client to set the Dependency during initialization.11 

4.4.4 节中的示例展示了如何处理这些并发症。但在此之前,我们想解释一下何时适合使用属性注入

The example in section 4.4.4 shows how you can deal with these complications. But before we get to that, we’d like to explain when it’s appropriate to use Property Injection.

4.4.2 何时使用属性注入

4.4.2 When to use Property Injection

仅当您正在开发的类具有良好的Local Default并且您仍然希望使调用者能够提供该类的Dependency的不同实现时,才应使用属性注入。重要的是要注意属性注入最好在依赖项可选的时候使用。如果需要依赖构造函数注入总是更好的选择。

Property Injection should only be used when the class you’re developing has a good Local Default, and you still want to enable callers to provide different implementations of the class’s Dependency. It’s important to note that Property Injection is best used when the Dependency is optional. If the Dependency is required, Constructor Injection is always a better pick.

在第 1 章中,我们讨论了编写松散耦合代码的充分理由,从而使模块彼此隔离。但是松散耦合也可以应用于单个模块中的类并取得巨大成功。这通常是通过在模块中引入抽象并让该模块中的类通过抽象进行通信来实现的,而不是彼此紧密耦合。在模块边界内应用松散耦合的主要原因是打开类以实现可扩展性和易于测试。

In chapter 1, we discussed good reasons for writing code with loose coupling, thus isolating modules from each other. But loose coupling can also be applied to classes within a single module with great success. This is often done by introducing Abstractions within a module and letting classes within that module communicate via Abstractions, instead of being tightly coupled to each other. The main reasons for applying loose coupling within a module boundary is to open classes for extensibility and for ease of testing.

到目前为止,我们还没有向您展示任何属性注入的真实示例,因为这种模式的适用性比较有限,尤其是在应用程序开发的上下文中。表 4.3总结了它的优点和缺点。

We haven’t shown you any real examples of Property Injection so far because the applicability of this pattern is more limited, especially in the context of application development. Table 4.3 summarizes its advantages and disadvantages.

表 4.3 属性注入的优点和缺点
优点缺点
容易明白 稳健实施并不完全简单

适用性有限

仅适用于可重用库

导致时间耦合

属性注入的主要优点是它很容易理解。当人们决定采用 DI 时,我们经常看到这种模式被用作第一次尝试。

The main advantage of Property Injection is that it’s so easy to understand. We’ve often seen this pattern used as a first attempt when people decide to adopt DI.

然而,外观可能具有欺骗性,而且属性注入充满了困难。以稳健的方式实施它具有挑战性。由于前面讨论的时间耦合问题,客户可能会忘记提供依赖项。此外,如果客户试图在类的生命周期中更改依赖项,会发生什么情况?这可能会导致不一致或意外的行为,因此您可能希望保护自己免受该事件的影响。

Appearances can be deceptive, though, and Property Injection is fraught with difficulties. It’s challenging to implement it in a robust manner. Clients can forget to supply the Dependency because of the previously discussed problem of Temporal Coupling. Additionally, what would happen if a client tries to change the Dependency in the middle of the class’s lifetime? This could lead to inconsistent or unexpected behavior, so you may want to protect yourself against that event.

尽管存在缺点,但在构建可重用库时使用属性注入是有意义的。它允许组件定义合理的默认值,这简化了使用库 API 的工作。

Despite the downsides, it makes sense to use Property Injection when building a reusable library. It allows components to define sensible defaults, and this simplifies working with a library’s API.

在开发应用程序时,您将类连接到Composition Root中。构造函数注入可防止您忘记提供依赖项。即使在存在Local Default的情况下,也可以将此类实例提供给Composition Root的构造函数。这简化了类并允许组合根控制所有消费者获得的值。这甚至可能是一个空对象实现。

When developing applications, you wire up your classes in your Composition Root. Constructor Injection prevents you from forgetting to supply the Dependency. Even in the case that there’s a Local Default, such instances can be supplied to the constructor by the Composition Root. This simplifies the class and allows the Composition Root to be in control over the value that all consumers get. This might even be a Null Object implementation.

良好的本地默认值的存在部分取决于模块的粒度。BCL 作为一个相当大的包裹运送;只要默认值保持在 BCL 范围内,就可以说它也是本地的。在下一节中,我们将简要介绍该主题。

The existence of a good Local Default depends in part on the granularity of modules. The BCL ships as a rather large package; as long as the default stays within the BCL, it could be argued that it’s also local. In the next section, we’ll briefly touch on that subject.

4.4.3属性注入的已知用途

4.4.3 Known uses of Property Injection

在 .NET BCL 中,Property InjectionConstructor Injection更常见一些,可能是因为在很多地方定义了良好的Local Defaults,也因为这简化了大多数类的默认实例化。例如,System.ComponentModel.IComponent具有可写Site属性允许您定义一个ISite实例。这主要用于设计时场景(例如,通过 Visual Studio)以在设计器中托管组件时更改或增强组件。以该 BCL 示例作为开胃菜,让我们继续使用和实现Property Injection的更实质性示例。

In the .NET BCL, Property Injection is a bit more common than Constructor Injection, probably because good Local Defaults are defined in many places, and also because this simplifies the default instantiation of most classes. For example, System.ComponentModel.IComponent has a writable Site property that allows you to define an ISite instance. This is mostly used in design time scenarios (for example, by Visual Studio) to alter or enhance a component when it’s hosted in a designer. With that BCL example as an appetizer, let’s move on to a more substantial example of using and implementing Property Injection.

4.4.4 示例:属性注入作为可重用库的扩展模型

4.4.4 Example: Property Injection as an extensibility model of a reusable library

本章前面的示例扩展了上一章的示例应用程序。尽管我们可以使用示例应用程序向您展示属性注入的示例,但这会产生误导,因为在构建应用程序时属性注入几乎不适合;构造函数注入几乎总是更好的选择。相反,我们想向您展示一个可重用库的示例。在这种情况下,我们正在查看来自 Simple Injector 的一些代码。

Earlier examples in this chapter extended the sample application of the previous chapter. Although we could show you an example of Property Injection using the sample application, this would be misleading because Property Injection is hardly ever a good fit when building applications; Constructor Injection is almost always a better choice. Instead, we’d like to show you an example of a reusable library. In this case, we’re looking at some code from Simple Injector.

Simple Injector 是第 4 部分中讨论的DI 容器之一。它可以帮助您构建应用程序的对象图。第 14 章将对 Simple Injector 进行广泛的讨论,因此我们不会在这里详细介绍它。从Property Injection的角度来看,Simple Injector 如何工作并不重要。

Simple Injector is one of the DI Containers that’s discussed in part 4. It helps you construct your application’s object graphs. Chapter 14 will have an extensive discussion on Simple Injector, so we won’t go into much detail about it here. From the perspective of Property Injection, how Simple Injector works isn’t important.

作为一个可重用的库,Simple Injector 广泛使用了Property Injection。它的很多行为都可以扩展,实现的方法是提供其行为的默认实现。Simple Injector 公开允许用户更改默认实现的属性。Simple Injector 允许替换的行为之一是库如何选择正确的构造函数来执行Constructor Injection14 

As a reusable library, Simple Injector makes extensive use of Property Injection. Lots of its behavior can be extended, and the way this is done is by providing default implementations of its behavior. Simple Injector exposes properties that allow the user to change the default implementation. One of the behaviors that Simple Injector allows to be replaced is how the library selects the correct constructor for doing Constructor Injection.14 

正如我们在 4.2 节中讨论的那样,您的类应该只有一个构造函数。因此,默认情况下,Simple Injector 只允许创建只有一个公共构造函数的类。在任何其他情况下,Simple Injector 都会抛出异常。然而,简单的注射器,让您覆盖此行为。这可能对某些狭窄的集成场景有用。为此,Simple Injector 定义了一个IConstructorResolutionBehavior接口. 15  自定义实现可以由用户定义,库提供的默认实现可以通过设置ConstructorResolutionBehavior属性来替换,如下所示:

As we discussed in section 4.2, your classes should only have one constructor. Because of this, Simple Injector, by default, only allows classes that have just one public constructor to be created. In any other case, Simple Injector throws an exception. Simple Injector, however, lets you override this behavior. This might be useful for certain narrow integration scenarios. For this, Simple Injector defines an IConstructorResolutionBehavior interface.15  A custom implementation can be defined by the user, and the library-provided default can be replaced by setting the ConstructorResolutionBehavior property, as shown here:

var container = new Container();

container.Options.ConstructorResolutionBehavior =
    new CustomConstructorResolutionBehavior();

Container是 Simple Injector API 中的中心 Facade 16  模式。它用于指定抽象和实现之间的关系,并构建这些实现的对象图。该类包含一个Options属性的类型。它包括许多允许更改库的默认行为的属性和方法。这些属性之一是. 这是该课程的简化版本ContainerOptionsConstructorResolutionBehaviorContainerOptions及其ConstructorResolutionBehavior财产:

The Container is the central Facade16  pattern in Simple Injector’s API. It’s used to specify the relationships between Abstractions and implementations, and to build object graphs of these implementations. The class includes an Options property of type ContainerOptions. It includes a number of properties and methods that allow the default behavior of the library to be changed. One of those properties is ConstructorResolutionBehavior. Here’s a simplified version of the ContainerOptions class with its ConstructorResolutionBehavior property:

public class ContainerOptions
{
    IConstructorResolutionBehavior resolutionBehavior =
        new DefaultConstructorResolutionBehavior();    ①  

    public IConstructorResolutionBehavior ConstructorResolutionBehavior
    {
        get
        {
            return this.resolutionBehavior;
        }
        set
        {
            if (value == null)    ②  
              throw new ArgumentNullException("value");

            if (this.Container.HasRegistrations)    ③  
            {
                throw new InvalidOperationException(
                    "The ConstructorResolutionBehav" +
                    "ior property cannot be changed" +
                    " after the first registration " +
                    "has been made to the container.";
            }

            this.resolutionBehavior = value;    ④  
        }
    }
}

ConstructorResolutionBehavior物业只要容器中没有注册,就可以多次更改。这很重要,因为在进行注册时,Simple Injector 使用指定的ConstructorResolutionBehavior通过分析类的构造函数来验证它是否能够构造这样的类型。如果用户能够在注册后更改构造函数解析行为,则可能会影响早期注册的正确性。这是因为 Simple Injector 最终可能会使用与它在注册时批准正确的组件不同的构造函数。这意味着要么重新评估所有以前的注册,要么阻止用户在注册后更改行为。因为重新评估可能有隐藏的性能成本并且更难实现,所以 Simple Injector 实现了后一种方法。

The ConstructorResolutionBehavior property can be changed multiple times as long as there are no registrations made in the container. This is important, because when registrations are made, Simple Injector uses the specified ConstructorResolutionBehavior to verify whether it’ll be able to construct such a type by analyzing a class’s constructor. If a user was able to change the constructor resolution behavior after registrations were made, it could impact the correctness of earlier registrations. This is because Simple Injector could, otherwise, end up using a different constructor for a component from that which it approved to be correct during the time of registration. This means that either all previous registrations should be reevaluated or the user should be prevented from being able to change the behavior after registrations are made. Because reevaluating can have hidden performance costs and is harder to implement, Simple Injector implements the latter approach.

Constructor Injection相比,Property Injection涉及更多。它的原始形式可能看起来很简单(如清单 4.19 所示),但是如果实现得当,它往往会更加复杂。

Compared to Constructor Injection, Property Injection is more involved. It may look simple in its raw form (as shown in listing 4.19), but, properly implemented, it tends to be more complex.

您在可重用库中使用Property Injection ,其中Dependency是可选的,并且您有一个很好的Local Default在存在需要Dependency的短期对象的情况下,您应该使用Method Injection。在其他情况下,您应该使用Constructor Injection

You use Property Injection in a reusable library where the Dependency is optional and you have a good Local Default. In cases where there’s a short-lived object that requires the Dependency, you should use Method Injection. In other cases, you should use Constructor Injection.

这完成了本章的最后一个模式。以下部分提供了一个简短的回顾,并解释了如何为您的工作选择正确的模式。

This completes the last pattern in this chapter. The following section provides a short recap and explains how to select the right pattern for your job.

4.5 选择要使用的模式

4.5 Choosing which pattern to use

本章介绍的模式是 DI 的核心部分。有了Composition Root和适当的注入模式组合,您就可以实现Pure DI或使用DI Container。应用 DI 时,有许多细微差别和更精细的细节需要学习,但这些模式涵盖了回答“如何注入我的依赖项?”这个问题的核心机制。

The patterns presented in this chapter are a central part of DI. Armed with a Composition Root and an appropriate mix of injection patterns, you can implement Pure DI or use a DI Container. When applying DI, there are many nuances and finer details to learn, but these patterns cover the core mechanics that answer the question, “How do I inject my Dependencies?”

这些模式不可互换。在大多数情况下,您的默认选择应该是Constructor Injection,但在某些情况下,其他模式之一可以提供更好的选择。图 4.9显示了一个决策过程,可以帮助您决定合适的模式,但是,如果有疑问,请选择Constructor Injection。你的选择不会错得离谱。

These patterns aren’t interchangeable. In most cases, your default choice should be Constructor Injection, but there are situations where one of the other patterns affords a better alternative. Figure 4.9 shows a decision process that can help you decide on a proper pattern, but, if in doubt, choose Constructor Injection. You can’t go terribly wrong with that choice.

04-11.eps

图 4.9 模式决策过程。在大多数情况下,您应该选择Constructor Injection,但在某些情况下,其他 DI 模式之一更适合。

Figure 4.9 Pattern decision process. In most cases, you should choose Constructor Injection, but there are situations where one of the other DI patterns is a better fit.

首先要检查的是依赖项是您需要的东西还是您已经拥有但想与其他合作者交流的东西。在大多数情况下,您可能需要Dependency。但在加载项方案中,您可能希望将当前上下文传达给加载项。每当依赖性因操作而异时,方法注入都是一个很好的实现候选者。

The first thing to examine is whether the Dependency is something you need or something you already have but want to communicate to another collaborator. In most cases, you’ll probably need the Dependency. But in add-in scenarios, you may want to convey the current context to an add-in. Every time the Dependency varies from operation to operation, Method Injection is a good candidate for an implementation.

其次,您需要知道什么样的类需要Dependency。如果您将运行时数据与同一个类中的行为混合在一起,就像您在域Entities中所做的那样,方法注入是一个很好的选择。在其他情况下,当您编写应用程序代码时,与编写可重用库相反,构造函数注入会自动应用。

Secondly, you’ll need to know what kind of class needs the Dependency. In case you’re mixing runtime data with behavior in the same class, as you might do in your domain Entities, Method Injection is a good fit. In other cases, when you’re writing application code, opposed to writing a reusable library, Constructor Injection automatically applies.

在编写应用程序代码时,甚至应该避免使用Local Defaults ,而是将这些默认值设置在应用程序的一个中心位置—— Composition Root。另一方面,在编写可重用库时,本地默认值是决定性因素,因为它可以显式分配依赖项可选——如果未指定覆盖实现,默认值将接管。这种场景可以通过Property Injection有效地实现。

When it comes to writing application code, even the use of Local Defaults should be prevented in favor of having these defaults set in one central place in the application — the Composition Root. On the other hand, when writing a reusable library, a Local Default is the deciding factor, as it can make explicitly assigning the Dependency optional — the default takes over if no overriding implementation is specified. This scenario can be effectively implemented with Property Injection.

构造函数注入应该是 DI 的默认选择。与任何其他 DI 模式相比,它易于理解且更易于稳健地实现。您可以单独使用构造函数注入构建整个应用程序,但了解其他模式可以帮助您在不完全适合的少数情况下做出明智的选择。下一章从相反的方向探讨 DI,并考察使用 DI 的不明智方式。

Constructor Injection should be your default choice for DI. It’s easy to understand and simpler to implement robustly than any of the other DI patterns. You can build entire applications with Constructor Injection alone, but knowing about the other patterns can help you choose wisely in the few cases where it doesn’t fit perfectly. The next chapter approaches DI from the opposite direction and takes a look at ill-advised ways of using DI.

概括

Summary

  • 组合根是应用程序中模块组合在一起的单个逻辑位置。应用程序组件的构造应该集中在应用程序的这个单一区域中。
  • The Composition Root is a single, logical location in an application where modules are composed together. The construction of your application’s components should be concentrated into this single area of your application.
  • 只有启动项目才会有Composition Root
  • Only startup projects will have a Composition Root.
  • 尽管组合根可以分布在多个类中,但它们应该位于单个模块中。
  • Although a Composition Root can be spread out across multiple classes, they should be in a single module.
  • Composition Root直接依赖于系统中的所有其他模块。与紧耦合代码相比,应用组合根模式的松耦合代码降低了模块、子系统和层之间的依赖性总数。
  • The Composition Root takes a direct dependency on all other modules in the system. Loosely coupled code that applies the Composition Root pattern lowers the overall number of dependencies between modules, subsystems, and layers, compared to tightly coupled code.
  • 即使您可能将合成根放置在与 UI 或表示层相同的程序集中,合成根也不是这些层的一部分。程序集是部署工件,而层是逻辑工件。
  • Even though you might place the Composition Root in the same assembly as your UI or presentation layer, the Composition Root isn’t part of those layers. Assemblies are deployment artifacts, whereas layers are logical artifacts.
  • 在使用DI 容器的地方,它应该只从Composition Root中引用。所有其他模块都应该忽略DI Container的存在。
  • Where a DI Container is used, it should only be referenced from the Composition Root. All other modules should be oblivious to the existence of the DI Container.
  • 在组合根之外使用DI 容器会导致服务定位器反模式。
  • Use of a DI Container outside the Composition Root leads to the Service Locator anti-pattern.
  • 使用DI 容器组合大型对象图的性能开销在设计良好的系统中通常不是问题。
  • The performance overhead of using a DI Container to compose large object graphs is usually not an issue in a well-designed system.
  • Composition Root应该是整个应用程序中唯一知道构造对象图结构的地方。这意味着应用程序代码无法将依赖项传递给与当前操作并行运行的其他线程,因为消费者无法知道这样做是否安全。相反,当分离并发操作时,组合根的工作是为每个并发操作创建一个新的对象图。
  • The Composition Root should be the sole place in the entire application that knows about the structure of the constructed object graphs. This means that application code can’t pass on Dependencies to other threads that run parallel to the current operation, because a consumer has no way of knowing whether it’s safe to do so. Instead, when spinning off concurrent operations, it’s the Composition Root’s job to create a new object graph for each concurrent operation.
  • 构造函数注入是通过将所需依赖项指定为类构造函数的参数来静态定义所需依赖项列表的行为。
  • Constructor Injection is the act of statically defining the list of required Dependencies by specifying them as parameters to the class’s constructor.
  • 用于构造函数注入的构造函数应该只应用保护子句并存储接收依赖项。其他逻辑应该保留在构造函数之外。这使得构建对象图变得快速而可靠。
  • A constructor that’s used for Constructor Injection should do no more than apply Guard Clauses and store the receiving Dependencies. Other logic should be kept out of the constructor. This makes building object graphs fast and reliable.
  • 构造函数注入应该是 DI 的默认选择,因为它是最可靠且最容易正确应用的。
  • Constructor Injection should be your default choice for DI, because it’s the most reliable and the easiest to apply correctly.
  • 当需要依赖时,构造函数注入非常适合。然而,重要的是要注意Dependencies几乎不应该是可选的。可选依赖项通过空检查使组件复杂化。在Composition Root内部,当没有可用的合理实现时,应该注入 Null Object 实现。
  • Constructor Injection is well suited when a Dependency is required. It’s important to note, however, that Dependencies should hardly ever be optional. Optional Dependencies complicate the component with null checks. Inside the Composition Root, a Null Object implementation should instead be injected when there’s no reasonable implementation available.
  • 应用程序组件应该只有一个构造函数。重载的构造函数会导致歧义。对于可重用的类库,比如 BCL,拥有多个构造函数通常是有意义的;对于应用程序组件,它没有。
  • Application components should only have a single constructor. Overloaded constructors lead to ambiguity. For reusable class libraries, like the BCL, having multiple constructors often makes sense; for application components, it doesn’t.
  • 方法注入是在方法调用上传递依赖关系的行为。
  • Method Injection is the act of passing Dependencies on method invocations.
  • 如果DependencyDependency的消费者对于每个操作可能不同,您可以应用Method Injection。这对于需要将某些运行时上下文传递到加载项的公共 API 的加载项场景,或者当以数据为中心的对象需要特定操作的依赖项时(域实体通常就是这种情况),这可能很有用.
  • Where either a Dependency or a Dependency’s consumer can differ for each operation, you can apply Method Injection. This can be useful for add-in scenarios where some runtime context needs to be passed along to the add-in’s public API, or when a data-centric object requires a Dependency for a certain operation, as will often be the case with domain Entities.
  • 方法注入不适合在组合根内部使用,因为它会导致时间耦合
  • Method Injection is unsuited for use inside the Composition Root because it leads to Temporal Coupling.
  • 通过方法注入接受依赖项的方法不应存储该依赖项。这会导致Temporal CouplingCaptive Dependencies或隐藏的副作用。依赖项只能与构造函数注入属性注入一起存储。
  • A method that accepts a Dependency through Method Injection shouldn’t store that Dependency. This leads to Temporal Coupling, Captive Dependencies, or hidden side effects. Dependencies should only be stored with Constructor Injection and Property Injection.
  • 本地默认是源自同一模块或层的依赖项的默认实现。
  • A Local Default is a default implementation of a Dependency that originates in the same module or layer.
  • 属性注入允许类库为扩展而开放,因为它允许调用者改变库的默认行为。
  • Property Injection allows class libraries to be open for extension, because it lets callers change the library’s default behavior.
  • Property Injection可能看起来很简单,但如果正确实施,它往往比Constructor Injection更复杂。
  • Property Injection may look simple, but when properly implemented, it tends to be more complex compared to Constructor Injection.
  • 除了可重用库中的可选依赖项之外,属性注入的适用性是有限的,而构造函数注入通常更合适。Constructor Injection简化了类,允许Composition Root控制所有消费者获得的值,并防止Temporal Coupling
  • Beyond optional Dependencies within reusable libraries, the applicability of Property Injection is limited, and Constructor Injection is usually a better fit. Constructor Injection simplifies the class, allows the Composition Root to be in control over the value that all consumers get, and prevents Temporal Coupling.

5

DI 反模式

5

DI anti-patterns

在这一章当中

In this chapter

  • 使用Control Freak创建紧密耦合的代码
  • Creating tightly coupled code with Control Freak
  • 使用服务定位器请求类的依赖项
  • Requesting a class’s Dependencies with a Service Locator
  • 使用Ambient Context使Volatile Dependency全局可用
  • Making a Volatile Dependency globally available with Ambient Context
  • 使用Constrained Construction强制特定的构造函数签名
  • Forcing a particular constructor signature with Constrained Construction

许多菜肴需要用油在平底锅中烹制。如果您对手头的食谱没有经验,您可以开始加热油,然后转身阅读食谱。但是一旦你切完蔬菜,油就会冒烟。您可能认为冒烟的油意味着锅很热,可以做饭了。这是没有经验的厨师的常见误解。当油开始冒烟时,它们也开始分解。这被称为他们的烟点。大多数油在加热超过烟点后不仅味道难闻,而且会形成有害化合物并失去有益的抗氧化剂。

Many dishes require food to be cooked in a pan with oil. If you’re not experienced with the recipe at hand, you might start heating the oil, and then turn your back to read the recipe. But once you’re done cutting the vegetables, the oil is smoking. You might think that the smoking oil means the pan is hot and ready for cooking. This is a common misconception with inexperienced cooks. When oils start to smoke, they also start to break down. This is called their smoke point. Not only do most oils taste awful once heated past their smoke point, they form harmful compounds and lose beneficial antioxidants.

在上一章中,我们简要地将设计模式与食谱进行了比较。模式提供了一种通用语言,我们可以使用它来简洁地讨论一个复杂的概念。当概念(或者更确切地说,实现)变得扭曲时,我们手上就有了一个反模式。

In the previous chapter, we briefly compared design patterns to recipes. A pattern provides a common language we can use to succinctly discuss a complex concept. When the concept (or rather, the implementation) becomes warped, we have an anti-pattern on our hands.

加热油超过其烟点是可视为烹饪反模式的典型示例。这是一个经常发生的错误。许多没有经验的厨师这样做是因为这似乎是一件合理的事情,但失去味道和不健康的食物都是负面后果。

Heating oil past its smoke point is a typical example of what can be considered to be a cooking anti-pattern. It’s a commonly occurring mistake. Many inexperienced cooks do this because it seems a reasonable thing to do, but loss of taste and unhealthful foods are negative consequences.

反模式或多或少是一种描述人们一再犯下的常见错误的形式化方式。在本章中,我们将描述一些与 DI 相关的常见反模式。在我们的职业生涯中,我们看到所有这些都以一种或其他形式使用,我们一直为自己应用所有这些而感到内疚。

Anti-patterns are, more or less, a formalized way of describing common mistakes that people make again and again. In this chapter, we’ll describe some common anti-patterns related to DI. During our career, we’ve seen all of them in use in one form or other, and we’ve been guilty of applying all of them ourselves.

在许多情况下,反模式代表了在应用程序中实现 DI 的真诚尝试。但由于不完全符合 DI 基础,这些实现可能会演变成弊大于利的解决方案。了解这些反模式可以让您了解在尝试第一个 DI 项目时需要注意哪些陷阱。但即使您多年来一直在应用 DI,仍然很容易出错。

In many cases, anti-patterns represent sincere attempts at implementing DI in an application. But because of not fully complying with DI fundamentals, the implementations can morph into solutions that do more harm than good. Learning about these anti-patterns can give you an idea about what traps to be aware of as you venture into your first DI projects. But even if you’ve been applying DI for years, it’s still easy to make mistakes.

可以通过将代码重构为第 4 章介绍的一种 DI 模式来修复反模式。修复每次出现的具体困难程度取决于实现的细节。对于每个反模式,我们将提供一些关于如何将其重构为更好模式的通用指导。

Anti-patterns can be fixed by refactoring the code toward one of the DI patterns introduced in chapter 4. Exactly how difficult it is to fix each occurrence depends on the details of the implementation. For each anti-pattern, we’ll supply some generalized guidance on how to refactor it toward a better pattern.

遗留代码有时需要采取严厉措施才能使您的代码可测试。这通常意味着采取小步骤来防止意外破坏以前工作的应用。在某些情况下,反模式可能是最合适的临时解决方案。尽管反模式的应用可能是对原始代码的改进,但重要的是要注意,这并不会使它成为反模式。存在其他记录在案且可重复的解决方案,这些解决方案被证明更有效。本章涵盖的反模式列于表 5.1中。

Legacy code sometimes requires drastic measures to make your code Testable. This often means taking small steps to prevent accidentally breaking a previously working application. In some cases, an anti-pattern might be the most appropriate temporary solution. Even though the application of an anti-pattern might be an improvement over the original code, it’s important to note that this doesn’t make it any less an anti-pattern; other documented and repeatable solutions exist that are proven to be more effective. The anti-patterns covered in this chapter are listed in table 5.1.

表 5.1 DI 反模式
反模式描述
控制狂与控制反转相反,依赖关系是直接控制的。
服务定位器隐式服务可以为消费者提供依赖项,但不能保证这样做。
环境语境通过静态访问器使单个依赖项可用。
约束构造假定构造函数具有特定的签名。

本章的其余部分将更详细地描述每个反模式,并按重要性排序。您可以从头到尾阅读,也可以只阅读您感兴趣的部分——每个部分都有一个独立的部分。如果您决定只阅读本章的一部分,我们建议您阅读Control FreakService Locator

The rest of this chapter describes each anti-pattern in greater detail, presenting them in order of importance. You can read from start to finish or only read the ones you’re interested in — each has a self-contained section. If you decide to read only part of this chapter, we recommend that you read Control Freak and Service Locator.

正如构造函数注入是最重要的 DI 模式一样,Control Freak是最常出现的反模式。它有效地阻止您应用任何类型的适当的 DI,所以我们需要在我们之前关注这个反模式向其他人讲话——你也应该如此。但是因为Service Locator看起来像是在解决一个问题,所以它是最危险的。我们将在 5.2 节中解决这个问题。

Just as Constructor Injection is the most important DI pattern, Control Freak is the most frequently occurring of the anti-patterns. It effectively prevents you from applying any kind of proper DI, so we’ll need to focus on this anti-pattern before we address the others — and so should you. But because Service Locator looks like it’s solving a problem, it’s the most dangerous. We’ll address that in section 5.2.

5.1 控制狂

5.1 Control Freak

控制反转的反义词是什么?控制反转一词最初是为了识别与正常情况相反的事物而创造的,但我们不能谈论“一切照旧”反模式。相反,Control Freak描述了一个不会放弃对其Volatile Dependencies的控制的类.

What’s the opposite of Inversion of Control? Originally the term Inversion of Control was coined to identify the opposite of the normal state of affairs, but we can’t talk about the “Business as Usual” anti-pattern. Instead, Control Freak describes a class that won’t relinquish control of its Volatile Dependencies.

例如,当您使用关键字创建Volatile Dependency的新实例时,就会出现Control Freak反模式。以下清单演示了Control Freak反模式的实现。new

As an example, the Control Freak anti-pattern happens when you create a new instance of a Volatile Dependency by using the new keyword. The following listing demonstrates an implementation of the Control Freak anti-pattern.

坏.tif

清单 5.1 一个Control Freak反模式示例

Listing 5.1 A Control Freak anti-pattern example

public class HomeController : Controller
{
    public ViewResult Index()
    {
        var service = new ProductService();    ①  

        var products = service.GetFeaturedProducts();
        return this.View(products);
    }
}

每次创建Volatile Dependency时,您都明确声明您将控制实例的生命周期,并且没有其他人有机会拦截该特定对象。虽然new关键字对于Volatile Dependencies来说是一种代码味道,您无需担心将其用于Stable Dependencies2个 

Every time you create a Volatile Dependency, you explicitly state that you’re going to control the lifetime of the instance and that no one else will get a chance to Intercept that particular object. Although the new keyword is a code smell when it comes to Volatile Dependencies, you don’t need to worry about using it for Stable Dependencies.2 

Control Freak最明显的例子是当你不努力在你的代码中引入抽象时。在第 2 章中,当 Mary 实现她的电子商务应用程序(第 2.1 节)时,您看到了几个这样的例子。这样的做法不尝试引入 DI。但即使在开发人员听说过 DI 和可组合性的地方,也经常可以在某些变体中找到Control Freak反模式。

The most blatant example of Control Freak is when you make no effort to introduce Abstractions in your code. You saw several examples of that in chapter 2 when Mary implemented her e-commerce application (section 2.1). Such an approach makes no attempt to introduce DI. But even where developers have heard about DI and composability, the Control Freak anti-pattern can often be found in some variation.

在接下来的部分中,我们将向您展示一些类似于我们在生产中使用过的代码的示例。在每一种情况下,开发人员都有最好的接口编程意图,但从未理解潜在的力量和动机。

In the next sections, we’ll show you some examples that resemble code we’ve seen used in production. In every case, the developers had the best intentions of programming to interfaces, but never understood the underlying forces and motivations.

5.1.1 例子:通过更新依赖来控制怪胎

5.1.1 Example: Control Freak through newing up Dependencies

许多开发人员听说过接口编程的原理,但不了解其背后更深层次的原理。为了做正确的事或遵循最佳实践,他们编写了没有多大意义的代码。例如,在清单 3.9 中,您看到了一个ProductService使用IProductRepository接口实例的示例检索特色产品列表。作为提醒,以下重复相关代码:

Many developers have heard about the principle of programming to interfaces but don’t understand the deeper rationale behind it. In an attempt to do the right thing or to follow best practices, they write code that doesn’t make much sense. For example, in listing 3.9, you saw an example of a ProductService that uses an instance of the IProductRepository interface to retrieve a list of featured products. As a reminder, the following repeats the relevant code:

public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
    return
        from product in this.repository.GetFeaturedProducts()
        select product.ApplyDiscountFor(this.userContext);
}

重点是成员变量代表一个Abstraction。在第 3 章中,您看到了如何通过构造函数注入填充字段,但我们已经看到了其他更简单的尝试。下面的清单显示了一种这样的尝试。repositoryrepository

The salient point is that the repository member variable represents an Abstraction. In chapter 3, you saw how the repository field can be populated via Constructor Injection, but we’ve seen other, more naïve attempts. The following listing shows one such attempt.

坏.tif

清单 5.2 新建一个ProductRepository

Listing 5.2 Newing up a ProductRepository

private readonly IProductRepository repository;

public ProductService()
{
    this.repository = new SqlProductRepository();    ①  
}

repository字段被声明为IProductRepository接口,因此ProductService类中的任何成员(例如)程序到接口。虽然这听起来像是正确的做法,但这样做并没有带来太多好处,因为在运行时,类型将始终是一个. 您无法拦截或更改变量GetFeaturedProductsSqlProductRepositoryrepository除非你更改代码并重新编译。此外,如果您将变量硬编码为始终具有特定的具体类型,那么通过将变量定义为抽象并不会获得太多好处。直接更新依赖关系Control Freak反模式的一个例子。

The repository field is declared as the IProductRepository interface, so any member in the ProductService class (such as GetFeaturedProducts) programs to an interface. Although this sounds like the right thing to do, not much is gained from doing so because, at runtime, the type will always be a SqlProductRepository. There’s no way you can Intercept or change the repository variable unless you change the code and recompile. Additionally, you don’t gain much by defining a variable as an Abstraction if you hard-code it to always have a specific concrete type. Directly newing up Dependencies is one example of the Control Freak anti-pattern.

在我们开始分析和解决由Control Freak产生的结果问题的可能方法之前,让我们看一些更多的例子,让您更好地了解上下文和常见的失败尝试。在下一个示例中,很明显解决方案不是最优的。大多数开发人员将尝试改进他们的方法。

Before we get to the analysis and possible ways to address the resulting issues generated by a Control Freak, let’s look at some more examples to give you a better idea of the context and common failed attempts. In the next example, it’s apparent that the solution isn’t optimal. Most developers will attempt to refine their approach.

5.1.2 示例:通过工厂控制 Freak

5.1.2 Example: Control Freak through factories

解决新建依赖项中明显问题的最常见和错误的尝试涉及某种工厂。对于工厂,有多种选择。我们将快速介绍以下各项:

The most common and erroneous attempt to fix the evident problems from newing up Dependencies involves a factory of some sort. When it comes to factories, there are several options. We’ll quickly cover each of the following:

  • 混凝土厂
  • Concrete Factory
  • 抽象工厂
  • Abstract Factory
  • 静态工厂
  • Static Factory

如果被告知她只能处理IProductRepository Abstraction,Mary Rowan(来自第 2 章)将介绍一个ProductRepositoryFactory可以生成她需要获取的实例的对象。让我们聆听她与同事 Jens 讨论这种方法的过程。我们预计他们的讨论将方便地涵盖我们列出的工厂选项。

If told that she could only deal with the IProductRepository Abstraction, Mary Rowan (from chapter 2) would introduce a ProductRepositoryFactory that would produce the instances she needs to get. Let’s listen in as she discusses this approach with her colleague Jens. We predict that their discussion will, conveniently, cover the factory options we’ve listed.

玛丽: 我们需要这个类中的一个实例IProductRepositoryProductService. ButIProductRepository是一个接口,所以我们不能只创建它的新实例,我们的顾问说我们不应该创建SqlProductRepository任何一个的新实例。

Mary: We need an instance of IProductRepository in this ProductService class. But IProductRepository is an interface, so we can’t just create new instances of it, and our consultant says that we shouldn’t create new instances of SqlProductRepository either.

Jens某种工厂怎么样?

Jens: What about some sort of factory?

玛丽是的,我也在想同样的事情,但我不确定如何进行。我不明白它是如何解决我们的问题的。看这里 -

Mary: Yes, I was thinking the same thing, but I’m not sure how to proceed. I don’t understand how it solves our problem. Look here —

Mary 开始编写一些代码来演示她的问题。这是玛丽写的代码:

Mary starts to write some code to demonstrate her problem. This is the code that Mary writes:

public class ProductRepositoryFactory
{
    public IProductRepository Create()
    {
        return new SqlProductRepository();
    }
}

混凝土厂

Concrete Factory

MaryProductRepositoryFactory封装了关于如何创建ProductRepository实例的知识,但它并没有解决问题,因为我们必须ProductService像这样使用它:

Mary: This ProductRepositoryFactory encapsulates knowledge about how to create ProductRepository instances, but it doesn’t solve the problem, because we’d have to use it in the ProductService like this:

var factory = new ProductRepositoryFactory();
this.repository = factory.Create();

看?现在我们需要在 中创建ProductRepositoryFactory该类的一个新实例ProductService,但这仍然对SqlProductRepository. 我们唯一取得的成就是将问题转移到另一个类中。

See? Now we need to create a new instance of the ProductRepositoryFactory class in the ProductService, but that still hard-codes the use of SqlProductRepository. The only thing we’ve achieved is moving the problem into another class.

Jens:是的,我明白了——我们不能用抽象工厂来解决问题吗?

Jens: Yes, I see — couldn’t we solve the problem with an Abstract Factory instead?

让我们暂停 Mary 和 Jens 的讨论,评估一下发生了什么。Mary 是完全正确的,混凝土工厂类不能解决Control Freak问题,而只能解决它。它在不增加任何价值的情况下使代码更加复杂。ProductService 现在直接控制工厂的生命周期,而工厂直接控制的生命周期ProductRepository,所以你仍然无法在运行时拦截或替换 Repository 实例。

Let’s pause Mary and Jens’ discussion to evaluate what happened. Mary is entirely correct that a Concrete Factory class doesn’t solve the Control Freak issue but only moves it around. It makes the code more complex without adding any value. ProductService now directly controls the lifetime of the factory, and the factory directly controls the lifetime of ProductRepository, so you still can’t Intercept or replace the Repository instance at runtime.

很明显,混凝土工厂不会解决任何 DI 问题,而且我们从未见过它以这种方式成功使用过。Jens 关于抽象工厂的评论听起来更有希望。

It’s fairly evident that a Concrete Factory won’t solve any DI problems, and we’ve never seen it used successfully in this fashion. Jens’ comment about Abstract Factory sounds more promising.

抽象工厂

Abstract Factory

让我们继续 Mary 和 Jens 的讨论,听听 Jens 对抽象工厂的看法。

Let’s resume Mary and Jens’ discussion and hear what Jens has to say about Abstract Factory.

Jens:如果我们像这样抽象工厂会怎么样?

Jens: What if we made the factory abstract, like this?

public interface IProductRepositoryFactory
{
    IProductRepository Create();
}

这意味着我们没有对 的任何引用进行硬编码SqlProductRepository,并且我们可以使用 中的工厂ProductService来获取 的实例IProductRepository

This means we haven’t hard-coded any references to SqlProductRepository, and we can use the factory in the ProductService to get instances of IProductRepository.

Mary:但是既然工厂是抽象的,我们如何获得它的新实例呢?

Mary: But now that the factory is abstract, how do we get a new instance of it?

Jens:我们可以创建一个返回SqlProductService实例的实现。

Jens: We can create an implementation of it that returns SqlProductService instances.

Mary:是的,但是我们如何创建它的实例呢?

Mary: Yes, but how do we create an instance of that?

Jens:我们刚刚在ProductService……哦。等待 -

Jens: We just new it up in the ProductService ... Oh. Wait —

玛丽:那会让我们回到起点。

Mary: That would put us back where we started.

Mary 和 Jens 很快意识到抽象工厂并不能改变他们的处境。他们最初的难题是他们需要一个 abstract 的实例,而现在他们需要一个 abstract 的实例。IProductRepositoryIProductRepositoryFactory

Mary and Jens quickly realize that an Abstract Factory doesn’t change their situation. Their original conundrum was that they needed an instance of the abstract IProductRepository, and now they need an instance of the abstract IProductRepositoryFactory instead.

既然 Mary 和 Jens 已经拒绝将抽象工厂作为一种可行的选择,那么一个破坏性的选择仍然存在。玛丽和詹斯即将得出结论。

Now that Mary and Jens have rejected the Abstract Factory as a viable option, one damaging option is still open. Mary and Jens are about to reach a conclusion.

静态工厂

Static Factory

让我们听听 Mary 和 Jens 决定他们认为可行的方法。

Let’s listen as Mary and Jens decide on an approach that they think will work.

玛丽:让我们做一个静态工厂。让我演示给你看:

Mary: Let’s make a Static Factory. Let me show you:

public static class ProductRepositoryFactory
{
    public static IProductRepository Create()
    {
        return new SqlProductRepository();
    }
}

既然类是静态的,我们就不需要处理如何创建它了。

Now that the class is static, we don’t need to deal with how to create it.

Jens:但是我们仍然硬编码我们返回SqlProductRepository实例,所以它对我们有任何帮助吗?

Jens: But we’ve still hard-coded that we return SqlProductRepository instances, so does it help us in any way?

Mary:我们可以通过确定ProductRepository要创建哪种类型的配置设置来处理这个问题。像这样:

Mary: We could deal with this via a configuration setting that determines which type of ProductRepository to create. Like this:

public static IProductRepository Create()
{
    IConfigurationRoot configuration = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json")
        .Build();

    string repositoryType = configuration["productRepository"];

    switch (repositoryType)
    {
        case "sql": return new SqlProductRepository();
        case "azure": return AzureProductRepository();
        default: throw new InvalidOperationException("...");
    }
}

看?这样我们就可以确定我们应该使用基于 SQL Server 的实现还是基于 Microsoft Azure 的实现,我们甚至不需要重新编译应用程序来从一个更改为另一个。

See? This way we can determine whether we should use the SQL Server–based implementation or the Microsoft Azure–based implementation, and we don’t even need to recompile the application to change from one to the other.

詹斯:酷!这就是我们要做的。那个顾问现在一定很高兴吧!

Jens: Cool! That’s what we’ll do. That consultant must be happy now!

有几个原因导致这样的静态工厂不能为接口编程的最初目标提供令人满意的解决方案。看一下图 5.1中的依赖关系图。

There are several reasons why such a Static Factory doesn’t provide a satisfactory solution to the original goal of programming to interfaces. Take a look at the Dependency graph in figure 5.1.

05-01.eps

图 5.1 建议解决方案的依赖图ProductRepositoryFactory

Figure 5.1 Dependency graph for the proposed ProductRepositoryFactory solution

所有类都需要引用抽象如下:IProductRepository

All classes need to reference the abstract IProductRepository as follows:

  • ProductService因为它消耗IProductRepository实例
  • ProductService because it consumes IProductRepository instances
  • ProductRepositoryFactory因为它创建IProductRepository实例
  • ProductRepositoryFactory because it creates IProductRepository instances
  • AzureProductRepository因为他们实施SqlProductRepositoryIProductRepository
  • AzureProductRepository and SqlProductRepository because they implement IProductRepository

ProductRepositoryFactory取决于类和类。因为直接依赖于,所以它也依赖于两个具体实现——回忆一下 4.1.4 节中的依赖是可传递的。AzureProductRepositorySqlProductRepositoryProductServiceProductRepositoryFactoryIProductRepository

ProductRepositoryFactory depends on both the AzureProductRepository and SqlProductRepository classes. Because ProductService directly depends on ProductRepositoryFactory, it also depends on both concrete IProductRepository implementations — recall from section 4.1.4 that dependencies are transitive.

只要ProductService依赖于 static ProductRepositoryFactory,就会遇到无法解决的设计问题。如果您ProductRepositoryFactory在域层中定义静态,则意味着域层需要依赖于数据访问层,因为ProductRepositoryFactory创建了一个SqlProductRepository位于该层中的对象。然而,数据访问层已经依赖于域层,因为它使用了与该层类似的SqlProductRepository类型和抽象。这会导致两个项目之间的循环引用。此外,如果你进入数据访问层,你仍然需要从领域层到数据访问层的依赖,因为依赖于. 这仍然会导致循环依赖。图 5.2ProductIProductRepositoryProductRepositoryFactoryProductServiceProductRepositoryFactory显示此设计问题。

As long as ProductService has a dependency on the static ProductRepositoryFactory, you have unsolvable design issues. If you define the static ProductRepositoryFactory in the domain layer, it means that the domain layer needs to depend on the data access layer, because ProductRepositoryFactory creates a SqlProductRepository that’s located in that layer. The data access layer, however, already depends on the domain layer because SqlProductRepository uses types and Abstractions like Product and IProductRepository from that layer. This causes a circular reference between the two projects. Additionally, if you move ProductRepositoryFactory into the data access layer, you still need a dependency from the domain layer to the data access layer because ProductService depends on ProductRepositoryFactory. This still causes a circular dependency. Figure 5.2 shows this design issue.

05-02.eps

图 5.2 由静态引起的域和数据访问层之间的循环依赖ProductRepositoryFactory

Figure 5.2 Cyclic dependency between the domain and the data access layers that’s caused by the static ProductRepositoryFactory

无论您如何移动类型,防止这些项目之间循环依赖的唯一方法是为所有类型创建一个项目。然而,这不是一个可行的选择,因为它将域层与数据访问层紧密耦合,并且不允许替换您的数据访问层。

No matter how you move your types around, the only way to prevent these circular dependencies between projects is by creating a single project for all types. This isn’t a viable option, however, because it tightly couples the domain layer to the data access layer and disallows your data access layer from being replaced.

Mary 和 Jens没有松散耦合的IProductRepository实现,而是以紧密耦合的模块结束。更糟糕的是,工厂总是拖延所有的实现——即使是那些不需要的!如果他们托管在 Azure 上,他们仍然需要将 Commerce.SqlDataAccess.dll(例如)与他们的应用程序一起分发。

Instead of loosely coupled IProductRepository implementations, Mary and Jens end up with tightly coupled modules. Even worse, the factory always drags along all implementations — even those that aren’t needed! If they host on Azure, they still need to distribute Commerce.SqlDataAccess.dll (for example) with their application.

如果 Mary 和 Jens 需要第三种类型的IProductRepository,他们将不得不更改工厂并重新编译他们的解决方案。尽管他们的解决方案可能是可配置的,但它是不可扩展的;如果一个单独的团队,甚至公司,需要创建一个新的存储库,他们将无法访问源代码。IProductRepository用特定于测试的实现替换具体实现也是不可能的,因为这需要IProductRepository在运行时定义实例,而不是在设计时静态地在配置文件中定义。

If Mary and Jens ever need a third type of IProductRepository, they’ll have to change the factory and recompile their solution. Although their solution may be configurable, it isn’t extensible; if a separate team, or even company, needs to create a new Repository, they’ll have no options without access to the source code. It’s also impossible to replace the concrete IProductRepository implementations with test-specific implementations, because that requires defining the IProductRepository instance at runtime, instead of statically in a configuration file at design time.

简而言之,静态工厂似乎可以解决问题,但实际上只会使问题复杂化。即使在最好的情况下,它也会迫使您引用Volatile Dependencies当重载构造函数与Foreign Defaults结合使用时,可以看到这种反模式的另一种变体,正如您将在下一个示例中看到的那样。

In short, a Static Factory may seem to solve the problem but, in reality, only compounds it. Even in the best cases, it forces you to reference Volatile Dependencies. Another variation of this anti-pattern can be seen when overloaded constructors are used in combination with Foreign Defaults, as you’ll see in the next example.

5.1.3 示例:通过重载构造函数控制 Freak

5.1.3 Example: Control Freak through overloaded constructors

构造函数重载在许多 .NET 代码库(包括 BCL)中相当普遍。通常,许多重载为一两个成熟的构造函数提供合理的默认值,这些构造函数将所有相关参数作为输入。(这种做法称为构造函数链接.) 有时,我们会看到 DI 的其他用途。

Constructor overloads are fairly common in many .NET code bases (including the BCL). Often, the many overloads provide reasonable defaults to one or two full-blown constructors that take all relevant parameters as input. (This practice is called Constructor Chaining.) At times, we see other uses when it comes to DI.

一个非常常见的反模式定义了一个特定于测试的构造函数重载,允许您显式定义Dependency,尽管生产代码使用无参数构造函数。当Dependency的默认实现代表Foreign Default而不是Local Default时,这可能是有害的。正如我们在 4.4.2 节中解释的那样,您通常希望使用构造函数注入来提供所有易失性依赖项——即使是那些可能是本地默认值的依赖项。

An all-too-common anti-pattern defines a test-specific constructor overload that allows you to explicitly define a Dependency, although the production code uses a parameterless constructor. This can be detrimental when the default implementation of the Dependency represents a Foreign Default rather than a Local Default. As we explained in section 4.4.2, you typically want to supply all Volatile Dependencies using Constructor Injection — even those that could be a Local Default.

以下清单显示了ProductService具有默认构造函数和重载构造函数的类。这是不该做什么的一个例子。

The following listing shows the ProductService class with a default and an overloaded constructor. It’s an example of what not to do.

坏.tif

ProductService具有多个构造函数的清单 5.3

Listing 5.3 ProductService with multiple constructors

private readonly IProductRepository repository;

public ProductService()    ①  
    : this(new SqlProductRepository())    ①  
{    ①  
}    ①  

public ProductService(IProductRepository repository)    ②  
{    ②  
    if (repository == null)    ②  
        throw new ArgumentNullException("repository");    ②  
    ②  
    this.repository = repository;    ②  
}    ②  

乍一看,这种编码风格似乎是两全其美。为了进行单元测试,它允许提供虚假的依赖项;然而,仍然可以方便地创建该类,而无需提供其Dependencies。下面的例子展示了这种风格:

At first sight, this coding style might seem like the best of both worlds. It allows fake Dependencies to be supplied for the sake of unit testing; whereas, the class can still be conveniently created without having to supply its Dependencies. The following example shows this style:

var productService = new ProductService();

通过让ProductService创建SqlProductRepository Volatile Dependency,您再次强制模块之间的强耦合。尽管ProductService可以在不同IProductRepository的实现中重复使用,但通过在测试时通过最灵活的构造函数重载提供它们,它会禁用拦截应用程序中IProductRepository实例的能力。

By letting ProductService create the SqlProductRepository Volatile Dependency, you again force strong coupling between modules. Although ProductService can be reused with different IProductRepository implementations, by supplying them via the most flexible constructor overload while testing, it disables the ability to Intercept the IProductRepository instance in the application.

既然您已经看到了Control Freak的一些示例,我们希望您对要查找的内容有更好的了解——new关键字的出现次数在易失性依赖项旁边。这可以使您避免最明显的陷阱。但是,如果您需要从这种反模式的现有事件中解脱出来,下一节将帮助您处理此类任务。

Now that you’ve seen a few of examples of Control Freak, we hope you have a better idea what to look for — occurrences of the new keyword next to Volatile Dependencies. This may enable you to avoid the most obvious traps. But if you need to untangle yourself from an existing occurrence of this anti-pattern, the next section will help you deal with such a task.

5.1.4控制狂分析

5.1.4 Analysis of Control Freak

Control FreakInversion of Control的对立面。当您直接控制Volatile Dependencies的创建时,您最终会得到紧密耦合的代码,而失去了第 1 章中概述的许多(如果不是全部)松散耦合的好处。

Control Freak is the antithesis of Inversion of Control. When you directly control the creation of Volatile Dependencies, you end up with tightly coupled code, missing many (if not all) of the benefits of loose coupling outlined in chapter 1.

Control Freak是最常见的 DI 反模式。它代表了大多数编程语言中创建实例的默认方式,因此即使在开发人员从未考虑过 DI 的应用程序中也可以观察到它。这是一种创建新对象的自然而根深蒂固的方式,许多开发人员发现很难放弃。即使当他们开始考虑 DI 时,他们也很难动摇他们必须以某种方式控制创建实例的时间和地点的心态。放开这种控制可能是一个艰难的精神飞跃;但是,即使您做到了,也有其他的陷阱需要避免,尽管这些陷阱较小。

Control Freak is the most common DI anti-pattern. It represents the default way of creating instances in most programming languages, so it can be observed even in applications where developers have never considered DI. It’s such a natural and deeply rooted way to create new objects that many developers find it difficult to discard. Even when they begin to think about DI, they have a hard time shaking the mindset that they must somehow control when and where instances are created. Letting go of that control can be a difficult mental leap to make; but, even if you make it, there are other, although lesser, pitfalls to avoid.

Control Freak反模式的负面影响

The negative effects of the Control Freak anti-pattern

由于Control Freak导致的紧密耦合代码,模块化设计的许多好处可能会丧失。这些在前面的每一节中都有介绍,但总结一下:

With the tightly coupled code that’s the result of Control Freak, many benefits of modular design are potentially lost. These were covered in each of the previous sections, but to summarize:

  • 尽管您可以将应用程序配置为使用多个预配置的Dependencies之一,但您不能随意替换它们。不可能提供在应用程序编译后创建的实现,当然也不可能提供特定实例作为实现。
  • Although you can configure an application to use one of multiple preconfigured Dependencies, you can’t replace them at will. It isn’t possible to provide an implementation that was created after the application was compiled, and it certainly isn’t possible to provide specific instances as an implementation.
  • 重用使用模块变得更加困难,因为它拖拽了在新上下文中可能不需要的依赖项。例如,考虑一个模块,该模块通过使用Foreign Default依赖于 ASP.NET Core 库。这使得将该模块重用为不应或不能依赖 ASP.NET Core 的应用程序(例如,Windows 服务或手机应用程序)的一部分变得更加困难。
  • It becomes harder to reuse the consuming module because it drags with it Dependencies that may be undesirable in the new context. As an example of this, consider a module that, through the use of a Foreign Default, depends on ASP.NET Core libraries. This makes it harder to reuse that module as part of an application that should’t or can’t depend on ASP.NET Core (for example, a Windows Service or mobile phone application).
  • 这使得并行开发更加困难。这是因为消费应用程序与其Dependencies的所有实现紧密耦合。
  • It makes parallel development more difficult. This is because the consuming application is tightly coupled to all implementations of its Dependencies.
  • 可测试性受到影响。Test Doubles 不能用作Dependency的替代品。
  • Testability suffers. Test Doubles can’t be used as substitutes for the Dependency.

通过仔细的设计,您仍然可以实现具有明确定义的职责的紧密耦合的应用程序,这样可维护性就不会受到影响。但即便如此,成本也太高了,而且你会保留很多限制。考虑到实现这一目标所需的工作量,没有理由继续投资于Control Freak。您需要远离Control Freak并转向适当的 DI。

With careful design, you can still implement tightly coupled applications with clearly defined responsibilities so that maintainability doesn’t suffer. But even so, the cost is too high, and you’ll retain many limitations. Given the amount of effort required to accomplish that, there’s no reason to continue investing in Control Freak. You need to move away from Control Freak and toward proper DI.

从Control Freak重构到 DI

Refactoring from Control Freak toward DI

要摆脱Control Freak,您需要将代码重构为第 4 章中介绍的正确 DI 设计模式之一。作为初始步骤,您应该使用图 4.9 中给出的指导来确定目标模式。在大多数情况下,这将是构造函数注入。重构步骤如下:

To get rid of Control Freak, you need to refactor your code toward one of the proper DI design patterns presented in chapter 4. As an initial step, you should use the guidance given in figure 4.9 to determine which pattern to aim for. In most cases, this will be Constructor Injection. The refactoring steps are as follows:

  1. 确保您正在编程为Abstraction。在示例中,情况已经如此;但在其他情况下,您可能需要先提取接口并更改变量声明。
  2. Ensure that you’re programming to an Abstraction. In the examples, this was already the case; but in other situations, you may need to first extract an interface and change variable declarations.
  3. 如果您在多个地方创建依赖项的特定实现,请将它们全部移动到一个创建方法中。确保此方法的返回值表示为抽象而不是具体类型。
  4. If you create a particular implementation of a Dependency in multiple places, move them all to a single creation method. Make sure this method’s return value is expressed as the Abstraction and not the concrete type.
  5. 现在您只有一个地方可以创建实例,通过实现其中一种 DI 模式(例如构造函数注入)将此创建移出使用类。
  6. Now that you have only a single place where you create the instance, move this creation out of the consuming class by implementing one of the DI patterns, such as Constructor Injection.

ProductService对于前面几节中的示例,构造函数注入是一个很好的解决方案。

In the case of the ProductService examples in the previous sections, Constructor Injection is an excellent solution.

好的.tif

清单 5.4使用构造函数注入 重构Control Freak

Listing 5.4 Refactoring away from Control Freak using Constructor Injection

public class ProductService : IProductService
{
    private readonly IProductRepository repository;

    public ProductService(IProductRepository repository)
    {
        if (repository == null)
            throw new ArgumentNullException("repository");

        this.repository = repository;
    }
}

Control Freak是迄今为止最具破坏性的反模式,但即使您控制了它,也会出现更微妙的问题。下一节将介绍更多反模式。尽管它们比Control Freak问题更少,但它们也往往更容易解决,因此请多加留意,并在发现它们时进行修复。

Control Freak is by far the most damaging anti-pattern, but even when you have it under control, more subtle issues can arise. The next sections look at more anti-patterns. Although they’re less problematic than Control Freak, they also tend to be easier to resolve, so be on the lookout, and fix them as they’re discovered.

5.2 服务定位器

5.2 Service Locator

很难放弃直接控制依赖项的想法,因此许多开发人员将静态工厂(如 5.1.2 节中描述的)提升到新的水平。这导致服务定位器反模式。

It can be difficult to give up on the idea of directly controlling Dependencies, so many developers take Static Factories (such as the one described in section 5.1.2) to new levels. This leads to the Service Locator anti-pattern.

作为最常见的实现,服务定位器是一个静态工厂,可以在第一个消费者开始使用它之前配置具体服务。(但您同样也会发现抽象的服务定位器。)可以想象,这可能发生在组合根中。根据特定的实现,服务定位器可以通过读取配置文件或使用它们的组合。以下清单显示了服务定位器反模式的实际应用。

As it’s most commonly implemented, the Service Locator is a Static Factory that can be configured with concrete services before the first consumer begins to use it. (But you’ll equally also find abstract Service Locators.) This could conceivably happen in the Composition Root. Depending on the particular implementation, the Service Locator can be configured with code by reading a configuration file or by using a combination thereof. The following listing shows the Service Locator anti-pattern in action.

坏.tif

清单 5.5 使用服务定位器反模式

Listing 5.5 Using the Service Locator anti-pattern

public class HomeController : Controller
{
    public HomeController() { }    ①  

    public ViewResult Index()
    {
        IProductService service =
            Locator.GetService<IProductService>();    ②  

        var products = service.GetFeaturedProducts();    ③  

        return this.View(products);
    }
}

不是静态定义所需的Dependencies列表,而是有一个无参数的构造函数,稍后请求它的Dependencies 。这对 的消费者隐藏了这些依赖关系,并且更难使用和测试。图 5.3显示了清单 5.5中的交互,您可以在其中看到服务定位器和实现之间的关系。HomeControllerHomeControllerHomeControllerProductService

Instead of statically defining the list of required Dependencies, HomeController has a parameterless constructor, requesting its Dependencies later. This hides these Dependencies from HomeController’s consumers and makes HomeController harder to use and test. Figure 5.3 shows the interaction in listing 5.5, where you can see the relationship between the Service Locator and the ProductService implementation.

05-03.eps

图 5.3HomeController服务定位器 之间的交互

Figure 5.3 Interaction between HomeController and Service Locator

多年前,将服务定位器称为反模式颇具争议。争论结束了:Service Locator是一种反模式。但是不要惊讶地发现到处都是这种反模式的代码库。

Years ago, it was quite controversial to call Service Locator an anti-pattern. The controversy is over: Service Locator is an anti-pattern. But don’t be surprised to find code bases that have this anti-pattern sprinkled all over the place.

重要的是要注意,如果您只查看类的静态结构,DI 容器看起来就像一个服务定位器。区别很微妙,不在于实现的机制,而在于你如何使用它。本质上,要求容器或定位器解析来自Composition Root的完整对象图是正确的用法。从除Composition Root以外的任何地方向它请求细粒度服务意味着服务定位器反模式。让我们回顾一个显示服务定位器运行的示例。

It’s important to note that if you look at only the static structure of classes, a DI Container looks like a Service Locator. The difference is subtle and lies not in the mechanics of implementation, but in how you use it. In essence, asking a container or locator to resolve a complete object graph from the Composition Root is proper usage. Asking it for granular services from anywhere else but the Composition Root implies the Service Locator anti-pattern. Let’s review an example that shows Service Locator in action.

5.2.1 示例:ProductService使用服务定位器

5.2.1 Example: ProductService using a Service Locator

让我们回到我们久经考验的ProductService,它需要一个接口实例IProductRepository。假设我们要应用服务定位器反模式,ProductService将使用静态GetService方法,如以下清单所示。

Let’s return to our tried-and-tested ProductService, which requires an instance of the IProductRepository interface. Assuming we were to apply the Service Locator anti-pattern, ProductService would use the static GetService method, as shown in the following listing.

坏.tif

清单 5.6在构造函数中 使用服务定位器

Listing 5.6 Using a Service Locator inside a constructor

public class ProductService : IProductService
{
    private readonly IProductRepository repository;

    public ProductService()
    {
        this.repository = Locator.GetService<IProductRepository>();
    }

    public IEnumerable<DiscountedProduct> GetFeaturedProducts() { ... }
}

在此示例中,我们GetService使用泛型类型参数来实现该方法,以指示所请求的服务类型。您也可以使用Type参数来指示类型,如果您更喜欢的话。

In this example, we implement the GetService method using generic type parameters to indicate the type of service being requested. You could also use a Type argument to indicate the type, if that’s more to your liking.

如以下清单所示,Locator该类的实现尽可能短。我们可以添加保护条款和错误处理,但我们想突出核心行为。该代码还可以包含一个功能,使它能够从文件中加载其配置,但我们将把它留给您作为练习。

As the following listing shows, this implementation of the Locator class is as short as possible. We could have added Guard Clauses and error handling, but we wanted to highlight the core behavior. The code could also include a feature that enables it to load its configuration from a file, but we’ll leave that as an exercise for you.

清单 5.7 一个简单的服务定位器实现

Listing 5.7 A simple Service Locator implementation

public static class Locator
{
    private static Dictionary<Type, object> services =    ①  
        new Dictionary<Type, object>();

    public static void Register<T>(T service)
    {
        services[typeof(T)] = service;
    }

    public static T GetService<T>()    ②  
    {    ②  
        return (T)services[typeof(T)];    ②  
    }

    public static void Reset()
    {
        services.Clear();
    }
}

诸如此类的客户端ProductService可以使用该GetService方法来请求抽象类型的实例T。因为此示例代码不包含保护子句或错误处理,所以如果请求的类型在字典中没有条目,则该方法会抛出一个相当神秘的错误。您可以想象如何添加代码以抛出更具描述性的异常。KeyNotFoundException

Clients such as ProductService can use the GetService method to request an instance of the abstract type T. Because this example code contains no Guard Clauses or error handling, the method throws a rather cryptic KeyNotFoundException if the requested type has no entry in the dictionary. You can imagine how to add code to throw a more descriptive exception.

GetService方法只能返回请求类型的实例,前提是它之前已插入到内部字典中。这可以用Register方法来完成. 同样,此示例代码不包含保护子句,因此可以Registernull,但更强大的实现不应允许这样做。此实现还永久缓存已注册的实例,但不难想出一个允许在每次调用时创建新实例的实现GetService。在某些情况下,尤其是单元测试时,能够重置服务定位器很重要。该功能由Reset方法提供, 清除内部字典。

The GetService method can only return an instance of the requested type if it has previously been inserted in the internal dictionary. This can be done with the Register method. Again, this example code contains no Guard Clause, so it would be possible to Register a null value, but a more robust implementation shouldn’t allow that. This implementation also caches registered instances forever, but it isn’t that hard to come up with an implementation that allows creating new instances on every call to GetService. In certain cases, particularly when unit testing, it’s important to be able to reset the Service Locator. That functionality is provided by the Reset method, which clears the internal dictionary.

类依赖于服务在服务定位器ProductService中可用,因此之前配置它很重要。在单元测试中,这可以通过存根实现的测试替身来完成,如下面的清单所示。7 

Classes like ProductService rely on the service to be available in the Service Locator, so it’s important that it’s previously configured. In a unit test, this could be done with a Test Double implemented by a Stub, as can be seen in the following listing.7 

坏.tif

清单 5.8 依赖于服务定位器的单元测试

Listing 5.8 A unit test depending on a Service Locator

[Fact]
public void GetFeaturedProductsWillReturnInstance()
{
    // Arrange
    var stub = ProductRepositoryStub();    ①  

    Locator.Reset();    ②  

    Locator.Register<IProductRepository>(stub);    ③  

    var sut = new ProductService();

    // Act
    var result = sut.GetFeaturedProducts();    ④  

    // Assert
    Assert.NotNull(result);
}

该示例显示了如何使用静态Register方法通过 Stub 实例配置服务定位器。如果这是在ProductService构建之前完成的,如示例所示,ProductService使用配置的存根来处理ProductRepository. 在完整的生产应用程序中,Service Locator将在Composition RootProductRepository中配置为正确的实现。

The example shows how the static Register method is used to configure the Service Locator with the Stub instance. If this is done before ProductService is constructed, as shown in the example, ProductService uses the configured Stub to work against ProductRepository. In the full production application, the Service Locator will be configured with the correct ProductRepository implementation in the Composition Root.

如果我们唯一的成功标准是Dependency可以随意使用和替换,那么这种从类中定位Dependencies的方法绝对有效。但它有一些严重的缺点。ProductService

This way of locating Dependencies from the ProductService class definitely works if our only success criterion is that the Dependency can be used and replaced at will. But it has some serious shortcomings.

5.2.2服务定位器分析

5.2.2 Analysis of Service Locator

服务定位器是一种危险的模式,因为它几乎可以工作。您可以从消费类中找到依赖项,并且可以用不同的实现替换这些依赖项——甚至可以使用单元测试中的测试替身。当您应用第 1 章中概述的分析模型来评估服务定位器是否可以匹配模块化应用程序设计的好处时,您会发现它适合大多数方面:

Service Locator is a dangerous pattern because it almost works. You can locate Dependencies from consuming classes, and you can replace those Dependencies with different implementations — even with Test Doubles from unit tests. When you apply the analysis model outlined in chapter 1 to evaluate whether Service Locator can match the benefits of modular application design, you’ll find that it fits in most regards:

  • 您可以通过更改注册来支持后期绑定。
  • You can support late binding by changing the registration.
  • 您可以并行开发代码,因为您是针对接口进行编程,可以随意替换模块。
  • You can develop code in parallel, because you’re programming against interfaces, replacing modules at will.
  • 您可以实现良好的关注点分离,因此没有什么能阻止您编写可维护的代码,但这样做会变得更加困难。
  • You can achieve good separation of concerns, so nothing stops you from writing maintainable code, but doing so becomes more difficult.
  • 您可以将依赖项替换为测试替身,从而确保可测试性。
  • You can replace Dependencies with Test Doubles, so Testability is ensured.

Service Locator仅在一个方面存在不足,因此不应掉以轻心。

There’s only one area where Service Locator falls short, and that shouldn’t be taken lightly.

服务定位器反模式的负面影响

Negative effects of the Service Locator anti-pattern

Service Locator的主要问题是它会影响使用它的类的可重用性。这表现在两个方面:

The main problem with Service Locator is that it impacts the reusability of the classes consuming it. This manifests itself in two ways:

  • 该类作为冗余Dependency沿Service Locator拖动。
  • The class drags along the Service Locator as a redundant Dependency.
  • 该类使其依赖项不明显。
  • The class makes it non-obvious what its Dependencies are.

让我们首先看一下5.2.1 节中示例的依赖关系图,如图 5.4所示。ProductService

Let’s first look at the Dependency graph for the ProductService from the example in section 5.2.1, shown in figure 5.4.

05-04.eps

图 5.4的 依赖关系ProductService

Figure 5.4 Dependency graph for a ProductService

除了预期的引用之外IProductRepositoryProductService还取决于Locator类。这意味着要重用ProductService该类,您不仅必须重新分配它及其相关的Dependency IProductRepository,而且还必须重新分配Locator Dependency,它仅出于机械原因而存在。如果该类是在与andLocator不同的模块中定义的,则想要重用的新应用程序也必须接受该模块。ProductServiceIProductRepositoryProductService

In addition to the expected reference to IProductRepository, ProductService also depends on the Locator class. This means that to reuse the ProductService class, you must redistribute not only it and its relevant Dependency IProductRepository, but also the Locator Dependency, which only exists for mechanical reasons. If the Locator class is defined in a different module than ProductService and IProductRepository, new applications wanting to reuse ProductService must accept that module too.

05-05.tif

图 5.5 Visual Studio 的 IntelliSense 唯一可以告诉我们关于ProductService类的事情是它有一个无参数的构造函数。它的依赖项是不可见的。

Figure 5.5 The only thing Visual Studio’s IntelliSense can tell us about the ProductService class is that it has a parameterless constructor. Its Dependencies are invisible.

也许我们甚至可以容忍这种额外的依赖关系是否Locator真的有必要让 DI 工作。我们会接受它作为为获得其他利益而支付的税款。但是有更好的选择(比如Constructor Injection)可用,所以这个Dependency是多余的。此外,对于想要使用该类的开发人员来说,这种冗余的依赖项和它的相关对应项都不是明确可见的。图 5.5显示 Visual Studio 没有提供有关使用此类的指导。IProductRepositoryProductService

Perhaps we could even tolerate that extra Dependency on Locator if it was truly necessary for DI to work. We’d accept it as a tax to be paid to gain other benefits. But there are better options (such as Constructor Injection) available, so this Dependency is redundant. Moreover, neither this redundant Dependency nor IProductRepository, its relevant counterpart, is explicitly visible to developers wanting to consume the ProductService class. Figure 5.5 shows that Visual Studio offers no guidance on the use of this class.

如果你想创建一个ProductService类的新实例, Visual Studio 只能告诉您该类具有无参数构造函数。但是如果您随后尝试运行代码,如果您忘记向该类注册IProductRepository实例,则会出现运行时错误Locator. 如果您不熟悉该ProductService课程,则很可能会发生这种情况。

If you want to create a new instance of the ProductService class, Visual Studio can only tell you that the class has a parameterless constructor. But if you subsequently attempt to run the code, you get a runtime error if you forgot to register an IProductRepository instance with the Locator class. This is likely to happen if you don’t intimately know the ProductService class.

该类ProductService远非自我记录:您无法判断在它起作用之前必须存在哪些依赖项。事实上,开发人员ProductService甚至可能决定在未来的版本中添加更多的Dependencies 。这意味着适用于当前版本的代码在未来的版本中可能会失败,并且您不会收到警告您的编译器错误。服务定位器很容易无意中引入重大更改。

The ProductService class is far from self documenting: you can’t tell which Dependencies must be present before it’ll work. In fact, the developers of ProductService may even decide to add more Dependencies in future versions. That would mean that code that works for the current version can fail in a future version, and you aren’t going to get a compiler error that warns you. Service Locator makes it easy to inadvertently introduce breaking changes.

使用泛型可能会让您认为服务定位器是强类型的。但即使像清单 5.7中所示的 API也是弱类型的,因为您可以请求任何类型。能够编译调用该GetService<T>方法的代码不能保证它不会在运行时左右抛出异常。

The use of generics may trick you into thinking that a Service Locator is strongly typed. But even an API like the one shown in listing 5.7 is weakly typed, because you can request any type. Being able to compile code invoking the GetService<T> method gives you no guarantee that it won’t throw exceptions left and right at runtime.

在进行单元测试时,您会遇到一个额外的问题,即在一个测试用例中注册的测试替身会导致 Interdependent Tests 代码异味,因为在执行下一个测试用例时它仍保留在内存中。因此有必要执行 Fixture Teardown每次测试后调用. 8  这是你必须手动记住的事情,而且很容易忘记。Locator.Reset()

When unit testing, you have the additional problem that a Test Double registered in one test case will lead to the Interdependent Tests code smell, because it remains in memory when the next test case is executed. It’s therefore necessary to perform Fixture Teardown after every test by invoking Locator.Reset().8  This is something that you must manually remember to do, and it’s easy to forget.

服务定位器可能看起来无害,但它可能导致各种令人讨厌的运行时错误。你如何避免这些问题?当您决定摆脱Service Locator时,您需要找到一种方法来做到这一点。一如既往,默认方法应该是构造函数注入,除非第 4 章中的其他 DI 模式之一提供了更好的匹配。

A Service Locator may seem innocuous, but it can lead to all sorts of nasty runtime errors. How do you avoid those problems? When you decide to get rid of a Service Locator, you need to find a way to do it. As always, the default approach should be Constructor Injection, unless one of the other DI patterns from chapter 4 provides a better fit.

从服务定位器重构到 DI

Refactoring from Service Locator toward DI

因为Constructor Injection静态声明类的Dependencies,它使代码在编译时失败,假设你练习Pure DI. 另一方面,当您使用DI 容器时,您将失去在编译时验证正确性的能力。但是,静态声明类的Dependencies仍然可以确保您可以通过要求容器为您创建所有对象图来验证应用程序对象图的正确性。您可以在应用程序启动时或作为单元/集成测试的一部分执行此操作。

Because Constructor Injection statically declares a class’s Dependencies, it enables the code to fail at compile time, assuming you practice Pure DI. When you use a DI Container, on the other hand, you lose the ability to verify correctness at compile time. Statically declaring a class’s Dependencies, however, still ensures that you can verify the correctness of your application’s object graphs by asking the container to create all object graphs for you. You can do this at application startup or as part of a unit/integration test.

一些DI 容器甚至更进一步,允许对 DI 配置进行更复杂的分析。这允许检测各种常见的陷阱。另一方面,Service LocatorDI Container是完全不可见的,因此它不可能代表您进行此类验证。

Some DI Containers even take this a step further and allow doing more-complex analysis on the DI configuration. This allows detecting all kinds of common pitfalls. A Service Locator, on the other hand, will be completely invisible to a DI Container, making it impossible for it to do these kinds of verification on your behalf.

在许多情况下,使用服务定位器的类可能会在其代码库中传播对它的调用。在这种情况下,它充当new声明的替代品。在这种情况下,第一个重构步骤是将每个依赖项的创建合并到一个方法中。

In many cases, a class that consumes a Service Locator may have calls to it spread throughout its code base. In such cases, it acts as a replacement for the new statement. When this is so, the first refactoring step is to consolidate the creation of each Dependency in a single method.

如果您没有成员字段来保存Dependency的实例,您可以引入这样一个字段并确保其余代码在使用Dependency时使用此字段。标记该字段以确保它不能在构造函数之外被修改。这样做会强制您使用服务定位器从构造函数分配字段。您现在可以引入一个构造函数参数来分配字段而不是服务定位器,然后可以将其删除。readonly

If you don’t have a member field to hold an instance of the Dependency, you can introduce such a field and make sure the rest of the code uses this field when it consumes the Dependency. Mark the field readonly to ensure that it can’t be modified outside the constructor. Doing so forces you to assign the field from the constructor using the Service Locator. You can now introduce a constructor parameter that assigns the field instead of the Service Locator, which can then be removed.

重构使用服务定位器的类类似于重构使用Control Freak的类。第 5.1.4 节包含有关重构Control Freak实现以使用 DI 的进一步说明。

Refactoring a class that uses Service Locator is similar to refactoring a class that uses Control Freak. Section 5.1.4 contains further notes on refactoring Control Freak implementations to use DI.

乍一看,Service Locator可能看起来像是一种合适的 DI 模式,但不要被愚弄:它可能明确地解决了松耦合问题,但同时也牺牲了其他问题。第 4 章中介绍的 DI 模式提供了更好的替代方案,缺点更少。服务定位器反模式以及本章介绍的其他反模式都是如此。尽管它们不同,但它们都有一个共同特征,即它们可以通过第 4 章中的一种 DI 模式来解决。

At first glance, Service Locator may look like a proper DI pattern, but don’t be fooled: it may explicitly address loose coupling, but it sacrifices other concerns along the way. The DI patterns presented in chapter 4 offer better alternatives with fewer drawbacks. This is true for the Service Locator anti-pattern, as well as the other anti-patterns presented in this chapter. Even though they’re different, they all share the common trait that they can be resolved by one of the DI patterns from chapter 4.

5.3 环境语境

5.3 Ambient Context

服务定位器相关的是环境上下文反模式。在服务定位器允许全局访问一组不受限制的依赖项的情况下,环境上下文通过静态访问器使单个强类型依赖项可用。

Related to Service Locator is the Ambient Context anti-pattern. Where a Service Locator allows global access to an unrestricted set of Dependencies, an Ambient Context makes a single strongly typed Dependency available through a static accessor.

以下清单显示了Ambient Context反模式的实际应用。

The following listing shows the Ambient Context anti-pattern in action.

坏.tif

清单 5.9 使用环境上下文反模式

Listing 5.9 Using the Ambient Context anti-pattern

public string GetWelcomeMessage()
{
    ITimeProvider provider = TimeProvider.Current;    ①  
    DateTime now = provider.Now;

    string partOfDay = now.Hour < 6 ? "night" : "day";

    return string.Format("Good {0}.", partOfDay);
}

在这个例子中,提出了一个允许检索系统当前时间的抽象。因为您可能想要影响应用程序感知时间的方式(例如,用于测试),所以您不想直接调用。一个好的解决方案不是让消费者直接调用,而是隐藏对抽象的访问。然而,允许消费者通过静态属性或方法访问默认实现是非常诱人的。在清单 5.9中,属性ITimeProviderDateTime.NowDateTime.NowDateTime.NowCurrent允许访问默认ITimeProvider实现。

In this example, ITimeProvider presents an Abstraction that allows retrieving the system’s current time. Because you might want to influence how time is perceived by the application (for instance, for testing), you don’t want to call DateTime.Now directly. Instead of letting consumers call DateTime.Now directly, a good solution is to hide access to DateTime.Now behind an Abstraction. It’s all too tempting, however, to allow consumers to access the default implementation through a static property or method. In listing 5.9, the Current property allows access to the default ITimeProvider implementation.

环境上下文在结构上类似于单例模式。9  两者都允许使用静态类成员访问依赖项。区别在于Ambient Context允许更改其Dependency,而 Singleton 模式确保其单一实例永不更改。

Ambient Context is similar in structure to the Singleton pattern.9  Both allow access to a Dependency by the use of static class members. The difference is that Ambient Context allows its Dependency to be changed, whereas the Singleton pattern ensures that its singular instance never changes.

访问系统的当前时间是一种常见的需求。让我们更深入地研究这个ITimeProvider例子。

The access to the system’s current time is a common need. Let’s dive a little bit deeper into the ITimeProvider example.

5.3.1 示例:通过环境上下文访问时间

5.3.1 Example: Accessing time through Ambient Context

人们需要对时间进行某种控制的原因有很多。许多应用程序的业务逻辑取决于时间或时间进程。在里面在前面的示例中,您看到了一个简单的案例,我们根据当前时间显示了一条欢迎消息。另外两个例子包括:

There are many reasons one would need to exercise some control over time. Many applications have business logic that depends on time or the progression of it. In the previous example, you saw a simple case where we displayed a welcome message based on the current time. Two other examples include these:

  • 基于星期几的成本计算。在某些企业中,客户在周末为服务支付更多费用是正常的。
  • Cost calculations based on day of the week. In some businesses, it’s normal for customers to pay more for services during the weekend.
  • 根据一天中的时间使用不同的通信渠道向用户发送通知。例如,企业可能希望在工作时间发送电子邮件通知,否则通过短信或寻呼机发送。
  • Sending notifications to users using different communication channels based on the time of day. For instance, the business might want email notifications to be sent during working hours, and by text message or pager, otherwise.

由于处理时间的需求如此普遍,因此开发人员常常感到有一种冲动,希望通过使用环境上下文来简化对此类易失性依赖项的访问。以下清单显示了一个示例AbstractionITimeProvider

Because the need to work with time is such a widespread requirement, developers often feel the urge to simplify access to such a Volatile Dependency by using an Ambient Context. The following listing shows an example ITimeProvider Abstraction.

清单 5.10 一个抽象ITimeProvider

Listing 5.10 An ITimeProviderAbstraction

public interface ITimeProvider
{
    DateTime Now { get; }    ①  
}

以下清单显示了TimeProviderITimeProvider 抽象类的简单实现。

The following listing shows a simplistic implementation of the TimeProvider class for this ITimeProvider Abstraction.

坏.tif

清单 5.11 环境TimeProvider上下文实现

Listing 5.11 A TimeProviderAmbient Context implementation

public static class TimeProvider    ①  
{
    private static ITimeProvider current =
        new DefaultTimeProvider();    ②  

    public static ITimeProvider Current    ③  
    {
        get { return current; }
        set { current = value; }
    }

    private class DefaultTimeProvider : ITimeProvider    ④  
    {
        public DateTime Now { get { return DateTime.Now; } }
    }
}

使用该实现,您可以对先前定义的方法进行单元测试TimeProviderGetWelcomeMessage. 下面的清单显示了这样的测试。

Using the TimeProvider implementation, you can unit test the previously defined GetWelcomeMessage method. The following listing shows such test.

坏.tif

清单 5.12 依赖于环境上下文的单元测试

Listing 5.12 A unit test depending on an Ambient Context

[Fact]
public void SaysGoodDayDuringDayTime()
{
    // Arrange
    DateTime dayTime = DateTime.Parse("2019-01-01 6:00");

    var stub = new TimeProviderStub { Now = dayTime };

    TimeProvider.Current = stub;    ①  

    var sut = new WelcomeMessageGenerator();    ②  

    // Act
    string actualMessage = sut.GetWelcomeMessage();    ③  

    // Assert
    Assert.Equal(expected: "Good day.", actual: actualMessage);
}

这是环境上下文反模式的一种变体。您可能遇到的其他常见变体是:

This is one variation of the Ambient Context anti-pattern. Other common variations you might encounter are these:

  • 允许消费者使用全局配置的Dependency行为的环境上下文考虑到前面的示例,TimeProvider可以为消费者提供静态GetCurrentTime方法通过在内部调用它来隐藏使用的依赖项。
  • An Ambient Context that allows consumers to make use of the behavior of a globally configured Dependency. With the previous example in mind, the TimeProvider could supply consumers with a static GetCurrentTime method that hides the used Dependency by calling it internally.
  • 将静态访问器与接口合并为单个抽象环境上下文对于前面的示例,这意味着您有一个包含实例属性和静态属性的基类。TimeProviderNowCurrent
  • An Ambient Context that merges the static accessor with the interface into a single Abstraction. In respect to the previous example, that would mean that you have a single TimeProvider base class that contains both the Now instance property and the static Current property.
  • 使用委托而不是自定义抽象环境上下文ITimeProvider您可以使用Func<DateTime>委托来实现相同的目的,而不是拥有一个相当描述性的界面。
  • An Ambient Context where delegates are used instead of a custom-defined Abstraction. Instead of having a fairly descriptive ITimeProvider interface, you could achieve the same using a Func<DateTime> delegate.

环境上下文可以有多种形式和实现方式。同样,关于Ambient Context的警告是它通过某些静态类成员提供对Volatile Dependency的直接或间接访问。在分析和评估解决由Ambient Context引起的问题的可能方法之前,让我们看一下Ambient Context的另一个常见示例。

Ambient Context can come in many shapes and implementations. Again, the caution regarding Ambient Context is that it provides either direct or indirect access to a Volatile Dependency by means of some static class member. Before doing the analysis and evaluating possible ways to fix the problems caused by Ambient Context, let’s look at another common example of Ambient Context.

5.3.2 示例:通过环境上下文记录

5.3.2 Example: Logging through Ambient Context

开发人员倾向于走捷径并步入环境上下文陷阱的另一个常见情况是在将日志记录应用于他们的应用程序时。任何真实的应用程序都需要能够将有关错误和其他不常见情况的信息写入文件或其他来源以供以后分析。许多开发人员认为日志记录是一项特殊的活动,值得“打破规则”。即使在非常熟悉 DI 的开发人员的代码库中,您也可能会发现类似于下一个清单中所示的代码。

Another common case where developers tend to take a shortcut and step into the Ambient Context trap is when it comes to applying logging to their applications. Any real application requires the ability to write information about errors and other uncommon conditions to a file or other source for later analysis. Many developers feel that logging is such a special activity that it deserves “bending the rules.” You might find code similar to that shown in the next listing even in the code bases of developers who are quite familiar with DI.

坏.tif

清单 5.13 记录时的环境上下文

Listing 5.13 Ambient Context when logging

public class MessageGenerator
{
    private static readonly ILog Logger =
        LogManager.GetLogger(typeof(MessageGenerator));  ①  

    public string GetWelcomeMessage()
    {
        Logger.Info("GetWelcomeMessage called.");    ②  

        return string.Format(
            "Hello. Current time is: {0}.", DateTime.Now);
    }
}

在日志记录方面, Ambient Context在许多应用程序中如此普遍存在有几个原因。首先,清单 5.13中的代码通常是日志库在其文档中显示的第一个示例。开发人员出于无知而复制了这些示例。我们不能责怪他们;开发人员通常假设库设计人员了解并交流最佳实践。不幸的是,情况并非总是如此。文档示例通常是为了简单而不是最佳实践而编写的,即使它们的设计者了解这些最佳实践也是如此。

There are several reasons why Ambient Context is so ubiquitous in many applications when it comes to logging. First, code like listing 5.13 is typically the first example that logging libraries show in their documentation. Developers copy those examples out of ignorance. We can’t blame them; developers typically assume that the library designers know and communicate best practices. Unfortunately, this isn’t always the case. Documentation examples are typically written for simplicity, not best practice, even if their designers understand those best practices.

除此之外,开发人员倾向于将Ambient Context应用于记录器,因为他们需要在应用程序中的几乎每个类中进行记录。在构造函数中注入它很容易导致构造函数具有太多Dependencies。这确实是一种称为构造函数过度注入的代码味道,我们将在第 6 章中讨论它。

Apart from that, developers tend to apply Ambient Context for loggers because they need logging in almost every class in their application. Injecting it in the constructor could easily lead to constructors with too many Dependencies. This is indeed a code smell called Constructor Over-injection, and we’ll discuss it in chapter 6.

Jeff Atwood 在 2008 年写了一篇关于伐木危险的精彩博文。10  他的一些论点如下:

Jeff Atwood wrote a great blog post back in 2008 about the danger of logging.10  A few of his arguments follow:

  • 日志记录意味着更多的代码,这会掩盖您的应用程序代码。
  • Logging means more code, which obscures your application code.
  • 日志记录不是免费的,大量日志记录意味着不断写入磁盘。
  • Logging isn’t free, and logging a lot means constantly writing to disk.
  • 你记录的越多,你能找到的就越少。
  • The more you log, the less you can find.
  • 如果值得保存到日志文件中,则值得在用户界面中显示。
  • If it’s worth saving to a log file, it’s worth showing in the user interface.

在处理 Stack Overflow 时,Jeff 删除了大部分日志记录,完全依赖未处理异常的日志记录。如果是错误,则应抛出异常。

When working on Stack Overflow, Jeff removed most of the logging, relying exclusively on logging of unhandled exceptions. If it’s an error, an exception should be thrown.

我们完全同意 Jeff 的分析,但也想从设计的角度来解决这个问题。我们发现,通过良好的应用程序设计,您将能够跨公共组件应用日志记录,而不会污染您的整个代码库。第 10 章详细描述了如何设计这样的应用程序。

We wholeheartedly agree with Jeff’s analysis, but would also like to approach this from a design perspective. We’ve found that with good application design, you’ll be able to apply logging across common components, without having it pollute your entire code base. Chapter 10 describes in detail how to design such an application.

Ambient Context的其他例子还有很多,但这两个例子是如此普遍和广泛,以至于我们在我们咨询过的公司中已经无数次看到它们。(我们过去甚至因自己引入Ambient Context实现而感到内疚。)既然您已经看到了Ambient Context的两个最常见的示例,下一节将讨论为什么它是一个问题以及如何处理它。

There are many other examples of Ambient Context, but these two examples are so common and widespread that we’ve seen them countless times in companies we’ve consulted with. (We’ve even been guilty of introducing Ambient Context implementations ourselves in the past.) Now that you’ve seen the two most common examples of Ambient Context, the next section discusses why it’s a problem and how to deal with it.

5.3.3环境语境分析

5.3.3 Analysis of Ambient Context

Ambient Context通常在开发人员有Cross-Cutting Concern时遇到作为Volatile Dependency,它被无处不在地使用。这种无处不在的特性使开发人员认为有理由放弃构造函数注入。它允许他们隐藏依赖项并避免将依赖项添加到应用程序中的许多构造函数的必要性。

Ambient Context is usually encountered when developers have a Cross-Cutting Concern as a Volatile Dependency, which is used ubiquitously. This ubiquitous nature makes developers think it justifies moving away from Constructor Injection. It allows them to hide Dependencies and avoids the necessity of adding the Dependency to many constructors in their application.

环境上下文反模式的负面影响

Negative effects of the Ambient Context anti-pattern

Ambient Context的问题与Service Locator的问题有关。以下是主要问题:

The problems with Ambient Context are related to the problems with Service Locator. Here are the main issues:

  • 依赖项是隐藏的。
  • The Dependency is hidden.
  • 测试变得更加困难。
  • Testing becomes more difficult.
  • 很难根据上下文更改依赖项。
  • It becomes hard to change the Dependency based on its context.
  • Dependency的初始化和使用之间存在Temporal Coupling
  • There’s Temporal Coupling between the initialization of the Dependency and its usage.

当您通过允许通过Ambient Context全局访问它来隐藏Dependency时,隐藏类具有太多Dependencies的事实变得更容易。这与构造函数过度注入代码异味有关,通常表明您违反了单一责任原则

When you hide a Dependency by allowing global access to it through Ambient Context, it becomes easier to hide the fact that a class has too many Dependencies. This is related to the Constructor Over-injection code smell and is typically an indication that you’re violating the Single Responsibility Principle.

当一个类有很多Dependencies时,这表明它做的比它应该做的多。理论上可以拥有一个具有许多Dependencies的类,同时仍然只有“一个改变的理由”。11  然而,班级越大,遵守该指南的可能性就越小。Ambient Context的使用隐藏了一个事实,即类可能变得过于复杂,需要重构。

When a class has many Dependencies, it’s an indication that it’s doing more than it should. It’s theoretically possible to have a class with many Dependencies, while still having just “one reason to change.”11  The larger the class, however, the less likely it is to abide by this guidance. The use of Ambient Context hides the fact that classes might have become too complex, and need to be refactored.

Ambient Context也使测试更加困难,因为它呈现了一个全局状态。当一个测试改变全局状态时,如您在清单 5.12中看到的,它可能会影响其他测试。当测试并行运行时就是这种情况,但是当测试忘记将其更改作为其拆卸的一部分时,即使是顺序执行的测试也会受到影响。尽管可以缓解这些与测试相关的问题,但这意味着构建特制的环境上下文以及全局或特定于测试的拆卸逻辑。这增加了复杂性,而替代方案则没有。

Ambient Context also makes testing more difficult because it presents a global state. When a test changes the global state, as you saw in listing 5.12, it might influence other tests. This is the case when tests run in parallel, but even sequentially executed tests can be affected when a test forgets to revert its changes as part of its teardown. Although these test-related issues can be mitigated, it means building a specially crafted Ambient Context and either global or test-specific teardown logic. This adds complexity, whereas the alternative doesn’t.

使用Ambient Context很难为不同的消费者提供不同的Dependency实现。例如,假设您需要系统的一部分在当前请求开始时固定的时刻工作,而其他可能长时间运行的操作应该获得实时更新的依赖项。12  为消费者提供依赖的不同实现正是清单 5.13中发生的事情,如这里重复:

The use of an Ambient Context makes it hard to provide different consumers with different implementations of the Dependency. For instance, say you need part of your system to work with a moment in time that’s fixed at the start of the current request, whereas other, possibly long-running operations, should get a Dependency that’s live-updated.12  Providing consumers with different implementations of the Dependency is exactly what happened in listing 5.13, as repeated here:

private static readonly ILog Logger =
    LogManager.GetLogger(typeof(MessageGenerator));

为了能够为消费者提供不同的实现,GetLoggerAPI 要求消费者传递其适当的类型信息。这不必要地使消费者复杂化。

To be able to provide consumers with different implementations, the GetLogger API requires the consumer to pass along its appropriate type information. This needlessly complicates the consumer.

环境上下文的使用会导致其在时间级别上耦合的依赖关系的使用。除非您在Composition Root中初始化Ambient Context ,否则应用程序会在该类首次开始使用Dependency时失败。相反,我们更希望我们的应用程序快速失败。

The use of an Ambient Context causes the usage of its Dependency coupled on a temporal level. Unless you initialize the Ambient Context in the Composition Root, the application fails when the class starts using the Dependency for the first time. We rather want our applications to fail fast instead.

尽管Ambient Context不像Service Locator那样具有破坏性,因为它只隐藏了一个Volatile Dependency而不是任意数量的Dependencies,所以它在设计良好的代码库中没有立足之地。总是有更好的选择,这就是我们在下一节中描述的内容。

Although Ambient Context isn’t as destructive as Service Locator, because it only hides a single Volatile Dependency opposed to an arbitrary number of Dependencies, it has no place in a well-designed code base. There are always better alternatives, which is what we describe in the next section.

从环境上下文重构到 DI

Refactoring from Ambient Context toward DI

即使在开发人员相当了解 DI 和服务定位器带来的危害的代码库中,看到Ambient Context也不要感到惊讶。很难说服开发人员放弃Ambient Context,因为他们已经习惯使用它。最重要的是,尽管针对 DI 重构单个类并不难,但诸如无效和有害的日志记录策略等潜在问题更难改变。通常,有很多代码出于并不总是很清楚的原因进行记录。当原始开发人员早已不在时,找出是否可以删除这些日志记录语句或者是否应该将其转化为异常通常是一个缓慢的过程。不过,假设代码库已经应用了 DI,从Ambient Context重构到 DI 是直截了当的。

Don’t be surprised to see Ambient Context even in code bases where the developers have a fairly good understanding of DI and the harm that Service Locator brings. It can be hard to convince developers to move away from Ambient Context, because they’re so accustomed to using it. On top of that, although refactoring a single class toward DI isn’t hard, the underlying problems like ineffective and harmful logging strategies are harder to change. Typically, there’s lots of code that logs for reasons that aren’t always clear. Finding out whether these logging statements could be removed or should be turned into exceptions instead can often be a slow process when the original developers are long gone. Still, assuming a code base already applies DI, refactoring away from Ambient Context toward DI is straightforward.

使用环境上下文的类通常包含一个或几个调用,可能分布在多个方法中。因为第一个重构步骤是集中对Ambient Context的调用,所以构造函数是执行此操作的好地方。

A class that consumes an Ambient Context typically contains one or a few calls to it, possibly spread over multiple methods. Because the first refactoring step is to centralize the call to the Ambient Context, the constructor is a good place to do this.

创建private readonly字段它可以保存对Dependency的引用并为其分配Ambient ContextDependency。该类的其余代码现在可以使用这个新的私有字段。现在可以将对环境上下文的调用替换为分配字段的构造函数参数和确保构造函数参数不为空的保护子句。这个新的构造函数参数可能会导致消费者中断。但是,如果已经应用了 DI,这只会导致组合根和类测试发生变化。以下清单显示了重构应用于.WelcomeMessageGenerator

Create a private readonly field that can hold a reference to the Dependency and assign it with the Ambient Context’s Dependency. The rest of the class’s code can now use this new private field. The call to the Ambient Context can now be replaced with a constructor parameter that assigns the field and a Guard Clause that ensures the constructor parameter isn’t null. This new constructor parameter will likely cause consumers to break. But if DI was applied already, this should only cause changes to the Composition Root and the class’s tests. The following listing shows the (unsurprising) result of the refactoring, when applied to the WelcomeMessageGenerator.

好的.tif

清单 5.14环境上下文重构为构造函数注入

Listing 5.14 Refactoring away from Ambient Context to Constructor Injection

public class WelcomeMessageGenerator
{
    private readonly ITimeProvider timeProvider;

    public WelcomeMessageGenerator(ITimeProvider timeProvider)
    {
        if (timeProvider == null)
            throw new ArgumentNullException("timeProvider");

        this.timeProvider = timeProvider;
    }

    public string GetWelcomeMessage()
    {
        DateTime now = this.timeProvider.Now;
        ...
    }
}

重构Ambient Context相对简单,因为在大多数情况下,您将在已经应用 DI 的应用程序中进行重构。对于不支持的应用程序,最好先解决Control Freak服务定位器问题,然后再处理环境上下文重构。

Refactoring Ambient Context is relatively simple because, for the most part, you’ll be doing it in an application that has already applied DI. For applications that don’t, it’s better to fix Control Freak and Service Locator problems first before tackling Ambient Context refactorings.

Ambient Context听起来是访问常用的Cross-Cutting Concerns的好方法,但看起来是骗人的。尽管与Control FreakService Locator相比问题较少,但Ambient Context通常是应用程序中较大设计问题的掩饰。第 4 章中描述的模式提供了更好的解决方案,在第 10 章中,我们将展示如何设计您的应用程序,使日志记录和其他横切关注点可以更轻松、更透明地应用于整个应用程序。

Ambient Context sounds like a great way to access commonly used Cross-Cutting Concerns, but looks are deceiving. Although less problematic than Control Freak and Service Locator, Ambient Context is typically a cover-up for larger design problems in the application. The patterns described in chapter 4 provide a better solution, and in chapter 10, we’ll show how to design your applications in such way that logging and other Cross-Cutting Concerns can be applied more easily and transparently across the application.

本章考虑的最后一个反模式是Constrained Construction。这通常源于实现后期绑定的愿望。

The last anti-pattern considered in this chapter is Constrained Construction. This often originates from the desire to attain late binding.

5.4 约束构造

5.4 Constrained Construction

正确实施 DI 的最大挑战是将所有具有Dependencies的类移动到Composition Root。当你做到这一点时,你已经走了很长一段路。尽管如此,仍有一些陷阱需要注意。

The biggest challenge of properly implementing DI is getting all classes with Dependencies moved to a Composition Root. When you accomplish this, you’ve already come a long way. Even so, there are still some traps to look out for.

一个常见的错误是要求依赖项具有具有特定签名的构造函数。这通常源于获得后期绑定的愿望,以便可以在外部配置文件中定义依赖关系,从而在不重新编译应用程序的情况下进行更改。

A common mistake is to require Dependencies to have a constructor with a particular signature. This normally originates from the desire to attain late binding so that Dependencies can be defined in an external configuration file and thereby changed without recompiling the application.

请注意,本节仅适用于需要后期绑定的场景。在直接从应用程序的根目录引用所有依赖项的情况下,您不会遇到此问题。但是话又说回来,如果不重新编译启动项目,您将无法替换依赖项。以下清单显示了Constrained Construction反模式的实际应用。

Be aware that this section applies only to scenarios where late binding is desired. In scenarios where you directly reference all Dependencies from the application’s root, you won’t have this problem. But then again, you won’t have the ability to replace Dependencies without recompiling the startup project, either. The following listing shows the Constrained Construction anti-pattern in action.

坏.tif

清单 5.15 Constrained Construction反模式示例

Listing 5.15 Constrained Construction anti-pattern example

public class SqlProductRepository : IProductRepository
{
    public SqlProductRepository(string connectionStr)    ①  
    {
    }
}

public class AzureProductRepository : IProductRepository
{
    public AzureProductRepository(string connectionStr)  ①  
    {
    }
}

IProductRepository 抽象的所有实现都必须具有具有相同签名的构造函数。在此示例中,构造函数应该只有一个 type 参数。尽管一个类具有type的Dependency是完全没问题的,但是强制这些实现具有相同的构造函数签名是一个问题。在 1.2.2 节中,我们简要地谈到了这个问题。本节对其进行更仔细的检查。stringstring

All implementations of the IProductRepository Abstraction are forced to have a constructor with the same signature. In this example, the constructor should have exactly one argument of type string. Although it’s perfectly fine for a class to have a Dependency of type string, it’s a problem for those implementations to be forced to have an identical constructor signature. In section 1.2.2, we briefly touched on this issue. This section examines it more carefully.

5.4.1 示例:后期绑定 ProductRepository

5.4.1 Example: Late binding a ProductRepository

在示例电子商务应用程序中,一些类依赖于IProductRepository接口. 这意味着要创建这些类,您首先需要创建一个IProductRepository实现。此时,您已经了解到组合根是执行此操作的正确位置。在 ASP.NET Core 应用程序中,这通常意味着Startup. 以下清单显示了创建IProductRepository.

In the sample e-commerce application, some classes depend on the IProductRepository interface. This means that to create those classes, you first need to create an IProductRepository implementation. At this point, you’ve learned that a Composition Root is the correct place to do this. In an ASP.NET Core application, this typically means Startup. The following listing shows the relevant part that creates an instance of an IProductRepository.

坏.tif

清单 5.16 隐式约束ProductRepository构造函数

Listing 5.16 Implicitly constraining the ProductRepository constructor

string connectionString = this.Configuration    ①  
    .GetConnectionString("CommerceConnectionString");    ①  

var settings =    ②  
    this.Configuration.GetSection("AppSettings");    ②  
    ②  
string productRepositoryTypeName =    ②  
    settings.GetValue<string>("ProductRepositoryType");  ②  

var productRepositoryType =    ③  
    Type.GetType(    ③  
        typeName: productRepositoryTypeName,    ③  
        throwOnError: true);    ③  

var constructorArguments =
    new object[] { connectionString };

IProductRepository repository =    ④  
    (IProductRepository)Activator.CreateInstance(    ④  
        productRepositoryType, constructorArguments);    ④  

以下代码显示了相应的配置文件:

The following code shows the corresponding configuration file:

{
  "ConnectionStrings": {
    "CommerceConnectionString":
      "Server=.;Database=MaryCommerce;Trusted_Connection=True;"
  },
  "AppSettings": {
    "ProductRepositoryType": "SqlProductRepository, Commerce.SqlDataAccess"
  },
}

应该引起怀疑的第一件事是从配置文件中读取连接字符串。ProductRepository如果您打算将 a视为抽象,为什么还需要连接字符串?

The first thing that should trigger suspicion is that a connection string is read from the configuration file. Why do you need a connection string if you plan to treat a ProductRepository as an Abstraction?

虽然这不太可能,但您可以选择ProductRepository使用内存数据库或 XML 文件来实现。基于 REST 的存储服务,如 Windows Azure 表存储服务,提供了一个更现实的选择,尽管今年最受欢迎的选择似乎还是关系数据库。数据库的无处不在使得人们很容易忘记连接字符串隐式表示实现选择。

Although it’s perhaps a bit unlikely, you could choose to implement a ProductRepository with an in-memory database or an XML file. A REST-based storage service, such as the Windows Azure Table Storage Service, offers a more realistic alternative, although, once again this year, the most popular choice seems to be a relational database. The ubiquity of databases makes it all too easy to forget that a connection string implicitly represents an implementation choice.

要后期绑定IProductRepository,您还需要确定选择了哪种类型作为实现。这可以通过从配置中读取程序集限定类型名称并Type从该名称创建实例来完成。这本身没有问题。当您需要创建该类型的实例时,困难就出现了。给定 a ,您可以使用该类Type创建一个实例Activator. 这创建实例方法调用类型的构造函数,因此您必须提供正确的构造函数参数以防止抛出异常。在这种情况下,您提供一个连接字符串。

To late bind an IProductRepository, you also need to determine which type has been chosen as the implementation. This can be done by reading an assembly-qualified type name from the configuration and creating a Type instance from that name. This in itself isn’t problematic. The difficulty arises when you need to create an instance of that type. Given a Type, you can create an instance using the Activator class. The CreateInstance method invokes the type’s constructor, so you must supply the correct constructor parameters to prevent an exception from being thrown. In this case, you supply a connection string.

如果您除了清单 5.16中的代码之外对该应用程序一无所知,您现在应该想知道为什么连接字符串作为构造函数参数传递给未知类型。如果实现是基于基于 REST 的 Web 服务或 XML 文件,那将毫无意义。

If you didn’t know anything else about the application other than the code in listing 5.16, you should by now be wondering why a connection string is passed as a constructor argument to an unknown type. It wouldn’t make sense if the implementation was based on a REST-based web service or an XML file.

实际上,这没有意义,因为这表示对Dependency的构造函数的意外约束。在这种情况下,您有一个隐含的要求,即 的任何实现都IProductRepository应该有一个将单个字符串作为输入的构造函数。这是对类必须派生自的显式约束的补充IProductRepository

Indeed, it doesn’t make sense because this represents an accidental constraint on the Dependency’s constructor. In this case, you have an implicit requirement that any implementation of IProductRepository should have a constructor that takes a single string as input. This is in addition to the explicit constraint that the class must derive from IProductRepository.

您可能会争辩说,IProductRepository基于 XML 文件的应用程序还需要一个字符串作为构造函数参数,尽管该字符串是文件名而不是连接字符串。但是,从概念上讲,它仍然很奇怪,因为您必须在connectionStrings配置元素中定义该文件名。(无论如何,我们认为这样的假设应该将 an作为构造函数参数而不是文件名。)XmlProductRepositoryXmlReader

You could argue that an IProductRepository based on an XML file would also require a string as constructor parameter, although that string would be a filename and not a connection string. But, conceptually, it’d still be weird because you’d have to define that filename in the connectionStrings element of the configuration. (In any case, we think such a hypothetical XmlProductRepository should take an XmlReader as a constructor argument instead of a filename.)

5.4.2约束构造分析

5.4.2 Analysis of Constrained Construction

在前面的例子中,隐式约束要求实现者有一个带有单个字符串参数的构造函数。一个更常见的约束是所有实现都应该有一个无参数的构造函数,这样最简单的形式就可以工作:Activator.CreateInstance

In the previous example, the implicit constraint required implementers to have a constructor with a single string parameter. A more common constraint is that all implementations should have a parameterless constructor, so that the simplest form of Activator.CreateInstance will work:

IProductRepository repository =
    (IProductRepository)Activator.CreateInstance(productRepositoryType);

虽然这可以说是最小的公分母,但灵活性的代价是巨大的。无论你如何限制对象构造,你都会失去灵活性。

Although this can be said to be the lowest common denominator, the cost in flexibility is significant. No matter how you constrain object construction, you lose flexibility.

Constrained Construction反模式的负面影响

Negative effects of the Constrained Construction anti-pattern

05-06.eps

图 5.6 您想要创建CommerceContext该类的单个实例并将该实例注入两个存储库。

Figure 5.6 You want to create a single instance of the CommerceContext class and inject that instance into both Repositories.

声明所有依赖项实现都应具有无参数构造函数可能很诱人。毕竟,它们可以在内部执行初始化;例如,直接从中读取连接字符串等配置数据配置文件。但这会在其他方面限制您,因为您可能希望将应用程序组合为封装其他实例的实例层。在某些情况下,例如,您可能希望在不同的消费者之间共享一个实例,如图 5.6 所示

It might be tempting to declare that all Dependency implementations should have a parameterless constructor. After all, they could perform their initialization internally; for example, reading configuration data like connection strings directly from the configuration file. But this would limit you in other ways because you might want to compose an application as layers of instances that encapsulate other instances. In some cases, for example, you might want to share an instance between different consumers, as illustrated in figure 5.6.

当您有多个类需要相同的Dependency时,您可能希望在所有这些类之间共享一个实例。这只有在您可以从外部注入该实例时才有可能。尽管您可以在这些类中的每一个中编写代码以从配置文件中读取类型信息并用于创建正确类型的实例,但以这种方式共享单个实例确实会涉及到。相反,同一类的多个实例会占用更多内存。Activator.CreateInstance

When you have more than one class requiring the same Dependency, you may want to share a single instance among all those classes. This is possible only when you can inject that instance from the outside. Although you could write code inside each of those classes to read type information from a configuration file and use Activator.CreateInstance to create the correct type of instance, it’d be really involved to share a single instance this way. Instead, you’d have multiple instances of the same class taking up more memory.

与其对对象的构造方式施加隐式约束,不如实现Composition Root,以便它可以处理您可能抛给它的任何类型的构造函数或工厂方法。现在让我们来看看如何重构以实现 DI。

Instead of imposing implicit constraints on how objects should be constructed, you should implement your Composition Root so that it can deal with any kind of constructor or factory method you may throw at it. Now let’s take a look at how you can refactor toward DI.

从约束构造重构到 DI

Refactoring from Constrained Construction toward DI

当您需要后期绑定时,您如何处理对组件的构造函数没有约束的问题?引入一个抽象工厂可能很诱人,它可以创建所需抽象的实例,然后要求这些抽象工厂的实现具有特定的构造函数签名。但是,这样做很可能会导致其自身的并发症。让我们研究一下这种方法。

How can you deal with having no constraints on components’ constructors when you need late binding? It may be tempting to introduce an Abstract Factory that can create instances of the required Abstraction and then require the implementations of those Abstract Factories to have a particular constructor signature. But doing so, however, is likely to cause complications of its own. Let’s examine such an approach.

想象一下使用抽象工厂进行IProductRepository 抽象。抽象工厂方案规定您还需要一个IProductRepositoryFactory接口. 图 5.7说明了这种结构。

Imagine using an Abstract Factory for the IProductRepository Abstraction. The Abstract Factory scheme dictates that you also need an IProductRepositoryFactory interface. Figure 5.7 illustrates this structure.

05-07.eps

图 5.7 尝试使用抽象工厂结构来解决后期绑定挑战

Figure 5.7 An attempt to use the Abstract Factory structure to solve the late-binding challenge

在此图中,代表真正的Dependency。但是为了使其实现者不受隐式约束,您尝试通过引入. 这将用于创建. 进一步的要求是任何工厂都有一个特定的构造函数签名。IProductRepositoryIProductRepositoryFactoryIProductRepository

In this figure, IProductRepository represents the real Dependency. But to keep its implementers free of implicit constraints, you attempt to solve the late-binding challenge by introducing an IProductRepositoryFactory. This will be used to create instances of IProductRepository. A further requirement is that any factories have a particular constructor signature.

现在让我们假设您想要使用 的实现,IProductRepository它需要一个 的实例IUserContext才能工作,如下一个清单所示。

Now let’s assume that you want to use an implementation of IProductRepository that requires an instance of IUserContext to work, as shown in the next listing.

清单 5.17 SqlProductRepository需要一个IUserContext

Listing 5.17 SqlProductRepository that requires an IUserContext

public class SqlProductRepository : IProductRepository
{
    private readonly IUserContext userContext;
    private readonly CommerceContext dbContext;

    public SqlProductRepository(
        IUserContext userContext, CommerceContext dbContext)
    {
        if (userContext == null)
            throw new ArgumentNullException("userContext");
        if (dbContext == null)
            throw new ArgumentNullException("dbContext");

        this.userContext = userContext;
        this.dbContext = dbContext;
    }
}

SqlProductRepository班级_实现IProductRepository接口,但需要. 因为唯一的构造函数不是无参构造函数,所以会派上用场。IUserContextIProductRepositoryFactory

The SqlProductRepository class implements the IProductRepository interface, but requires an instance of IUserContext. Because the only constructor isn’t a parameterless constructor, IProductRepositoryFactory will come in handy.

当前,您希望使用IUserContext基于 ASP.NET Core 的实现。你称这个实现为(正如我们在代码清单 3.12 中所讨论的)。因为实现依赖于 ASP.NET Core,所以它没有在与. 而且,因为您不想将对包含的库的引用与 一起拖动,所以唯一的解决方案是在与 不同的程序集中实现,如图 5.8所示。AspNetUserContextAdapterSqlProductRepositoryAspNetUserContextAdapterSqlProductRepositorySqlProductRepositoryFactorySqlProductRepository

Currently, you want to use an implementation of IUserContext that’s based on ASP.NET Core. You call this implementation AspNetUserContextAdapter (as we discussed in listing 3.12). Because the implementation depends on ASP.NET Core, it isn’t defined in the same assembly as SqlProductRepository. And, because you don’t want to drag a reference to the library that contains AspNetUserContextAdapter along with SqlProductRepository, the only solution is to implement SqlProductRepositoryFactory in a different assembly than SqlProductRepository, as shown in figure 5.8.

05-08.eps

图 5.8SqlProductRepositoryFactory在单独程序集中实现的 依赖关系图

Figure 5.8 Dependency graph with SqlProductRepositoryFactory implemented in a separate assembly

以下清单显示了.SqlProductRepository-Factory

The following listing shows a possible implementation for the SqlProductRepository-Factory.

坏.tif

清单 5.18 创建SqlProductRepository实例的工厂

Listing 5.18 Factory that creates SqlProductRepository instances

public class SqlProductRepositoryFactory
    : IProductRepositoryFactory
{
    private readonly string connectionString;

    public SqlProductRepositoryFactory(
        IConfigurationRoot configuration)    ①  
    {
        this.connectionString =
            configuration.GetConnectionString(    ②  
                "CommerceConnectionString");
    }

    public IProductRepository Create()
    {
        return new SqlProductRepository(    ③  
            new AspNetUserContextAdapter(),
            new CommerceContext(this.connectionString));
    }
}

尽管IProductRepositoryIProductRepositoryFactory看起来像一对紧密结合的对象,但在两个不同的程序集中实现它们很重要。这是因为工厂必须引用所有依赖项才能将它们正确连接在一起。按照惯例,IProductRepositoryFactory实现必须再次使用约束构造,以便您可以在配置文件中写入程序集限定类型名称并用于Activator.CreateInstance创建实例。

Even though IProductRepository and IProductRepositoryFactory look like a cohesive pair, it’s important to implement them in two different assemblies. This is because the factory must have references to all Dependencies to be able to wire them together correctly. By convention, the IProductRepositoryFactory implementation must again use Constrained Construction so that you can write the assembly-qualified type name in a configuration file and use Activator.CreateInstance to create an instance.

每次您需要将Dependencies的新组合连接在一起时,您必须实现一个新工厂来准确连接该组合,然后将应用程序配置为使用该工厂而不是以前的工厂。这意味着您不能在不编写和编译代码的情况下定义任意组合的依赖关系,但您可以在不重新编译应用程序本身的情况下做到这一点。这样的抽象工厂成为在与核心应用程序分开的程序集中定义的抽象组合根。虽然这是可能的,但当您尝试应用它时,您会注意到它导致的不灵活。

Every time you need to wire together a new combination of Dependencies, you must implement a new factory that wires up exactly that combination, and then configure the application to use that factory instead of the previous one. This means you can’t define arbitrary combinations of Dependencies without writing and compiling code, but you can do it without recompiling the application itself. Such an Abstract Factory becomes an Abstract Composition Root that’s defined in an assembly separate from the core application. Although this is possible, when you try to apply it, you’ll notice the inflexibility that it causes.

灵活性受到影响,因为抽象组合根直接依赖于其他库中的具体类型来满足它构建的对象图的需要。在示例中,工厂需要创建一个实例以传递给. 但是,如果核心应用程序想要替换或拦截实现怎么办?这会强制更改核心应用程序和项目。另一个问题是这些抽象工厂很难管理对象生命周期这与图 5.5所示的问题相同。SqlProductRepositoryFactoryAspNetUserContextAdapterSqlProductRepositoryIUserContextSqlProductRepositoryFactory

Flexibility suffers because the Abstract Composition Root takes direct dependencies on concrete types in other libraries to fulfill the needs of the object graphs it builds. In the SqlProductRepositoryFactory example, the factory needs to create an instance of AspNetUserContextAdapter to pass to SqlProductRepository. But what if the core application wants to replace or Intercept the IUserContext implementation? This forces changes to both the core application and the SqlProductRepositoryFactory project. Another problem is that it becomes quite hard for these Abstract Factories to manage Object Lifetime. This is the same problem as illustrated in figure 5.5.

为了解决这种不灵活的问题,唯一可行的解​​决方案是使用通用的DI 容器。因为DI 容器使用反射分析构造函数签名,所以抽象组合根不需要知道用于构造其组件的依赖项。Abstract Composition Root唯一需要做的就是指定抽象和实现之间的映射。换句话说,SQL 数据访问Composition Root需要指定,以防万一应用程序需要一个IProductRepositorySqlProductRepository应该创建一个实例。

To combat this inflexibility, the only feasible solution is to use a general-purpose DI Container. Because DI Containers analyze constructor signatures using reflection, the Abstract Composition Root doesn’t need to know the Dependencies used to construct its components. The only thing the Abstract Composition Root needs to do is specify the mapping between the Abstraction and the implementation. In other words, the SQL data access Composition Root needs to specify that in case the application requires an IProductRepository, an instance of SqlProductRepository should be created.

仅当您确实需要能够插入新程序集而无需重新编译现有应用程序的任何部分时,才需要抽象组合根。大多数应用程序不需要这种灵活性。尽管您可能希望能够将 SQL 数据访问层替换为 Azure 数据访问层,而无需重新编译域层,但如果这意味着您仍然需要对启动项目进行更改,这通常是可以的。

Abstract Composition Roots are only required when you truly need to be able to plug in a new assembly without having to recompile any part of the existing application. Most applications don’t need this amount of flexibility. Although you might want to be able to replace the SQL data access layer with an Azure data access layer without having to recompile the domain layer, it’s typically OK if this means you still have to make changes to the startup project.

因为 DI 是一组模式和技术,所以没有任何一种工具可以机械地验证您是否正确应用了它。在第 4 章中,我们研究了描述如何正确使用 DI 的模式,但这只是问题的一方面。研究失败的可能性也很重要,即使是出于好意。你可以从失败中吸取重要的教训,但你不必总是从自己的错误中吸取教训——有时你可以从别人的错误中吸取教训。

Because DI is a set of patterns and techniques, no single tool can mechanically verify whether you’ve applied it correctly. In chapter 4, we looked at patterns that describe how DI can be used properly, but that’s only one side of the coin. It’s also important to study how it’s possible to fail, even with the best of intentions. You can learn important lessons from failure, but you don’t have to always learn from your own mistakes — sometimes you can learn from other people’s mistakes.

在本章中,我们以反模式的形式描述了最常见的依赖注入错误。我们在现实生活中不止一次看到过所有这些错误,我们承认犯下了所有这些错误。到现在为止,您应该知道应该避免什么以及理想情况下应该做什么。但是,仍然可能存在看起来难以解决的问题。下一章将讨论这些挑战以及如何解决它们。

In this chapter, we’ve described the most common DI mistakes in the form of anti-patterns. We’ve seen all these mistakes in real life on more than one occasion, and we confess to being guilty of all of them. By now, you should know what to avoid and what you should ideally be doing instead. There can still be issues that look as though they’re hard to solve, however. The next chapter discusses such challenges and how to resolve them.

概括

Summary

  • 模式是对产生明显负面后果的问题的常见解决方案的描述。
  • An anti-pattern is a description of a commonly occurring solution to a problem that generates decidedly negative consequences.
  • Control Freak是本章介绍的最主要的反模式。它有效地阻止您应用任何类型的适当 DI。每当您在Composition Root以外的任何地方依赖 Volatile Dependency时,它都会发生。
  • Control Freak is the most dominating of the anti-patterns presented in this chapter. It effectively prevents you from applying any kind of proper DI. It occurs every time you depend on a Volatile Dependency in any place other than a Composition Root.
  • 虽然new关键字在涉及Volatile Dependencies时是一种代码味道,但您无需担心将其用于Stable Dependencies。在一般来说,该new关键字不会突然变得非法,但您应该避免使用它来获取Volatile Dependencies的实例。
  • Although the new keyword is a code smell when it comes to Volatile Dependencies, you don’t need to worry about using it for Stable Dependencies. In general, the new keyword isn’t suddenly illegal, but you should refrain from using it to get instances of Volatile Dependencies.
  • Control Freak违反了依赖倒置原则
  • Control Freak is a violation of the Dependency Inversion Principle.
  • Control Freak代表了大多数编程语言中创建实例的默认方式,因此即使在开发人员从未考虑过 DI 的应用程序中也可以观察到它。这是一种创建新对象的自然而根深蒂固的方式,许多开发人员发现很难放弃。
  • Control Freak represents the default way of creating instances in most programming languages, so it can be observed even in applications where developers have never considered DI. It’s such a natural and deeply rooted way to create new objects that many developers find it difficult to discard.
  • 外国违约本地违约相反。它是作为默认值使用的依赖项的实现,即使它是在与其使用者不同的模块中定义的。拖拽不需要的模块会使您失去松散耦合的许多好处。
  • A Foreign Default is the opposite of a Local Default. It’s an implementation of a Dependency that’s used as a default even though it’s defined in a different module than its consumer. Dragging along unwanted modules robs you of many of the benefits of loose coupling.
  • 服务定位器是本章介绍的最危险的反模式,因为它看起来像是在解决一个问题。它为Composition Root之外的应用程序组件提供访问一组无限制的Volatile Dependencies的权限。
  • Service Locator is the most dangerous anti-pattern presented in this chapter because it looks like it’s solving a problem. It supplies application components outside the Composition Root with access to an unbounded set of Volatile Dependencies.
  • 服务定位器影响使用它的组件的可重用性。它使组件的消费者无法清楚地知道它的依赖项是什么,使这样的组件对其复杂程度不诚实,并导致其消费组件作为冗余依赖项拖到服务定位器上。
  • Service Locator impacts the reusability of the components consuming it. It makes it non-obvious to a component’s consumers what its Dependencies are, makes such a component dishonest about its level of complexity, and causes its consuming components to drag along the Service Locator as a redundant Dependency.
  • 服务定位器阻止验证类之间关系的配置。Constructor Injection结合Pure DI允许在编译时进行验证;构造函数注入DI 容器相结合,允许在应用程序启动时或作为简单自动化测试的一部分进行验证。
  • Service Locator prevents verification of the configuration of relationships between classes. Constructor Injection in combination with Pure DI allows verification at compile time; Constructor Injection in combination with a DI Container allows verification at application startup or as part of a simple automated test.
  • 静态服务定位器会导致相互依赖的测试,因为在执行下一个测试用例时它会保留在内存中。
  • A static Service Locator causes Interdependent Tests, because it remains in memory when the next test case is executed.
  • 将其确定为服务定位器的不是 API 的机械结构,而是 API 在应用程序中扮演的角色。因此,封装在组合根中的DI 容器不是服务定位器——它是基础结构组件。
  • It’s not the mechanical structure of an API that determines it as a Service Locator, but rather the role the API plays in the application. Therefore, a DI Container encapsulated in a Composition Root isn’t a Service Locator — it’s an infrastructure component.
  • Ambient Context通过使用静态类成员为Composition Root外部的应用程序代码提供对Volatile Dependency或其行为的全局访问。
  • Ambient Context supplies application code outside the Composition Root with global access to a Volatile Dependency or its behavior by using static class members.
  • Ambient Context在结构上类似于 Singleton 模式,但Ambient Context允许更改其依赖项。单例模式确保单个创建的实例永远不会改变。
  • Ambient Context is similar in structure to the Singleton pattern with the exception that Ambient Context allows its Dependency to be changed. The Singleton pattern ensures that the single created instance will never change.
  • 当开发人员将横切关注点作为普遍使用的依赖项时,通常会遇到环境上下文,这使他们认为这有理由摆脱构造函数注入
  • Ambient Context is usually encountered when developers have a Cross-Cutting Concern as a Dependency that’s used ubiquitously, making them think it justifies moving away from Constructor Injection.
  • Ambient Context导致Volatile Dependency变得隐藏,使测试复杂化,并且难以根据其上下文更改Dependency 。
  • Ambient Context causes the Volatile Dependency to become hidden, complicates testing, and makes it difficult to change the Dependency based on its context.
  • Constrained Construction强制某个抽象的所有实现都具有特定的构造函数签名,以实现后期绑定。它限制了灵活性,并可能迫使实现在内部进行初始化。
  • Constrained Construction forces all implementations of a certain Abstraction to have a particular constructor signature with the goal of enabling late binding. It limits flexibility and might force implementations to do their initialization internally.
  • 可以通过使用通用DI 容器来防止约束构造,因为DI 容器使用反射来分析构造函数签名。
  • Constrained Construction can be prevented by utilizing a general-purpose DI Container because DI Containers analyze constructor signatures using reflection.
  • 如果您可以重新编译启动项目,则应将组合根集中在启动项目中,并避免使用后期绑定。后期绑定引入了额外的复杂性,而复杂性增加了维护成本。
  • If you can get away with recompiling the startup project, you should keep your Composition Root centralized in the startup project and refrain from using late binding. Late binding introduces extra complexity, and complexity increases maintenance costs.

6

代码味道

6

Code smells

在这一章当中

In this chapter

  • 处理构造函数过度注入的代码异味
  • Handling Constructor Over-injection code smells
  • 检测和防止过度使用抽象工厂
  • Detecting and preventing overuse of Abstract Factories
  • 修复循环依赖代码的味道
  • Fixing cyclic Dependency code smells

你可能已经注意到我(马克)对蛋黄酱或荷兰酱很着迷。一个原因是它的味道很好;另一个是制作起来有点棘手。除了生产方面的挑战之外,它还提出了一个完全不同的问题:必须立即提供服务(或者我是这么认为的)。

You may have noticed that I (Mark) have a fascination with sauce béarnaise — or sauce hollandaise. One reason is that it tastes so good; another is that it’s a bit tricky to make. In addition to the challenges of production, it presents an entirely different problem: it must be served immediately (or so I thought).

当客人到达时,这曾经不太理想。我没有能够随便问候我的客人,让他们感到受欢迎和放松,而是在厨房里疯狂地搅打酱汁,让他们自娱自乐。经过几次重复表演后,我善于交际的妻子决定自己动手。我们住在一家餐馆的街对面,所以有一天她和厨师聊天,想知道是否有什么技巧可以让我提前准备好真正的荷兰酱。原来有。现在,我可以为我的客人提供美味的酱汁,而无需先让他们感受到压力和狂热的气氛。

This used to be less than ideal when guests arrived. Instead of being able to casually greet my guests and make them feel welcome and relaxed, I was frantically whipping the sauce in the kitchen, leaving them to entertain themselves. After a couple of repeat performances, my sociable wife decided to take matters into her own hands. We live across the street from a restaurant, so one day she chatted with the cooks to find out whether there’s a trick that would enable me to prepare a genuine hollandaise well in advance. It turns out there is. Now I can serve a delicious sauce for my guests without first subjecting them to an atmosphere of stress and frenzy.

每种工艺都有自己的交易技巧。一般来说,对于软件开发,尤其是 DI,也是如此。挑战不断涌现。在许多情况下,有众所周知的方法来处理它们。多年来,我们看到人们在学习 DI 时遇到困难,并且许多问题在本质上都是相似的。在本章中,我们将了解将 DI 应用于代码库时出现的最常见的代码异味,以及如何解决它们。当我们完成后,您应该能够更好地识别和处理这些情况。

Each craft has its own tricks of the trade. This is also true for software development, in general, and for DI, in particular. Challenges keep popping up. In many cases, there are well-known ways to deal with them. Over the years, we’ve seen people struggle when learning DI, and many of the issues were similar in nature. In this chapter, we’ll look at the most common code smells that appear when you apply DI to a code base and how you can resolve them. When we’re finished, you should be able to better recognize and handle these situations when they occur.

与本书这一部分的前两章类似,本章以目录的形式组织——这次是问题和解决方案(或者,如果你愿意的话,重构)的目录。您可以根据自己的喜好独立或按顺序阅读每个部分。每个部分的目的都是让您熟悉常见问题的解决方案,以便您在问题发生时能够更好地处理它。但首先,让我们定义代码味道。

Similar to the two previous chapters in this part of the book, this chapter is organized as a catalog — this time, a catalog of problems and solutions (or, if you will, refactorings). You can read each section independently or in sequence, as you prefer. The purpose of each section is to familiarize you with a solution to a commonly occurring problem so that you’ll be better equipped to deal with it if it occurs. But first, let’s define code smells.

模式是对产生明显负面后果的问题的常见解决方案的描述,另一方面,代码气味是可能导致问题的代码结构。代码味道只需要进一步调查。

Where an anti-pattern is a description of a commonly occurring solution to a problem that generates decidedly negative consequences, a code smell, on the other hand, is a code construct that might cause problems. Code smells simply warrant further investigation.

6.1 处理构造函数过度注入的代码异味

6.1 Dealing with the Constructor Over-injection code smell

除非您有特殊要求,否则构造函数注入(我们在第 4 章中介绍过)应该是您首选的注入模式。尽管构造函数注入很容易实现和使用,但当他们的构造函数开始看起来像下图所示时,它会让开发人员感到不舒服。

Unless you have special requirements, Constructor Injection (we covered this in chapter 4) should be your preferred injection pattern. Although Constructor Injection is easy to implement and use, it makes developers uncomfortable when their constructors start looking something like that shown next.

气味.tif

清单 6.1具有许多依赖项 的构造函数

Listing 6.1 Constructor with many Dependencies

public OrderService(
    IOrderRepository orderRepository,    ①  
    IMessageService messageService,    ①  
    IBillingSystem billingSystem,    ①  
    ILocationService locationService,    ①  
    IInventoryManagement inventoryManagement)    ①  
{
    if (orderRepository == null)
        throw new ArgumentNullException("orderRepository");
    if (messageService == null)
        throw new ArgumentNullException("messageService");
    if (billingSystem == null)
        throw new ArgumentNullException("billingSystem");
    if (locationService == null)
        throw new ArgumentNullException("locationService");
    if (inventoryManagement == null)
        throw new ArgumentNullException("inventoryManagement");

    this.orderRepository = orderRepository;
    this.messageService = messageService;
    this.billingSystem = billingSystem;
    this.locationService = locationService;
    this.inventoryManagement = inventoryManagement;
}

有很多依赖关系表明单一职责原则(建议零售价) 违反。违反 SRP 会导致代码难以维护。

Having many Dependencies is an indication of a Single Responsibility Principle (SRP) violation. SRP violations lead to code that’s hard to maintain.

在本节中,我们将研究构造函数参数数量不断增加的明显问题,以及为什么构造函数注入是好事而不是坏事。正如您将看到的,这并不意味着您应该在构造函数中接受长参数列表,因此我们还将回顾您可以对这些列表做些什么。您可以通过多种方式重构构造函数过度注入,因此我们还将讨论您可以采用的两种常见方法来重构这些事件,即 Facade Services 和领域事件:

In this section, we’ll look at the apparent problem of a growing number of constructor parameters and why Constructor Injection is a good thing rather than a bad thing. As you’ll see, it doesn’t mean you should accept long parameter lists in constructors, so we’ll also review what you can do about those. You can refactor away from Constructor Over-injection in many ways, so we’ll also discuss two common approaches you can take to refactor those occurrences, namely, Facade Services and domain events:

  •   Facade Services 是与参数对象相关的抽象 Facades 1 。2  然而,Facade Service 不是组合组件并将它们作为参数公开,而是仅公开封装的行为,同时隐藏成分。
  • Facade Services are abstract Facades1  that are related to Parameter Objects.2  Instead of combining components and exposing them as parameters, however, a Facade Service exposes only the encapsulated behavior, while hiding the constituents.
  • 使用域事件,您可以捕获可以触发您正在开发的应用程序状态发生变化的操作。
  • With domain events, you capture actions that can trigger a change to the state of the application you’re developing.

6.1.1 认识构造函数过度注入

6.1.1 Recognizing Constructor Over-injection

当构造函数的参数列表变得太大时,我们将这种现象称为构造函数过度注入,并将其视为代码异味。3  这是一个与 DI 无关但被 DI 放大的普遍问题。尽管您最初的反应可能是因为构造函数过度注入而放弃构造函数注入,但我们应该庆幸向我们揭示了一个一般的设计问题。

When a constructor’s parameter list grows too large, we call the phenomenon Constructor Over-injection and consider it a code smell.3  It’s a general issue unrelated to, but magnified by, DI. Although your initial reaction might be to dismiss Constructor Injection because of Constructor Over-injection, we should be thankful that a general design issue is revealed to us.

我们不能因为不喜欢清单 6.1中所示的构造函数而责怪任何人,但不要责怪构造函数注入。我们可以同意具有五个参数的构造函数是一种代码味道,但它表明违反了 SRP 而不是与 DI 相关的问题。

We can’t say we blame anyone for disliking a constructor as shown in listing 6.1, but don’t blame Constructor Injection. We can agree that a constructor with five parameters is a code smell, but it indicates a violation of the SRP rather than a problem related to DI.

我们个人的门槛是四个构造函数参数。当我们添加第三个参数时,我们已经开始考虑是否可以设计不同的东西,但我们可以接受几个类的四个参数。您的极限可能不同,但当您超过极限时,就该进行调查了。

Our personal threshold lies at four constructor arguments. When we add a third argument, we already begin considering whether we could design things differently, but we can live with four arguments for a few classes. Your limit may be different, but when you cross it, it’s time to investigate.

如何重构已经变得太大的特定类取决于特定情况:已经存在的对象模型、领域、业务逻辑等等。分裂一个崭露头角的神级根据众所周知的设计模式进入更小、更集中的类始终是一个很好的举措。4  不过,在某些情况下,业务需求迫使您同时做许多不同的事情。在应用程序的边界处通常会出现这种情况。考虑触发许多业务事件的粗粒度 Web 服务操作。

How you refactor a particular class that has grown too big depends on the particular circumstances: the object model already in place, the domain, business logic, and so on. Splitting up a budding God Class into smaller, more focused classes according to well-known design patterns is always a good move.4  Still, there are cases where business requirements oblige you to do many different things at the same time. This is often the case at the boundary of an application. Think about a coarse-grained web service operation that triggers many business events.

您可以设计和实施合作者,使他们不违反 SRP。在第 9 章中,我们将讨论 Decorator 5  设计模式如何帮助您堆叠横切关注点,而不是将它们作为服务注入到消费者中。这可以消除许多构造函数参数。在某些场景下,单个入口点需要编排许多Dependencies。一个示例是触发许多不同服务的复杂交互的 Web 服务操作。计划批处理作业的入口点可能面临同样的问题。

You can design and implement collaborators so that they don’t violate the SRP. In chapter 9, we’ll discuss how the Decorator5  design pattern can help you stack Cross-Cutting Concerns instead of injecting them into consumers as services. This can eliminate many constructor arguments. In some scenarios, a single entry point needs to orchestrate many Dependencies. One example is a web service operation that triggers a complex interaction of many different services. The entry point of a scheduled batch job can face the same issue.

我们不时查看的示例电子商务应用程序需要能够接收订单。这通常最好由单独的应用程序或子系统来完成,因为此时事务的语义会发生变化。只要你在看一个购物篮,你就可以动态地计算出单价、汇率和折扣。但是当客户下订单时,所有这些值都必须被捕获并冻结,因为它们在客户批准订单时呈现。表 6.1提供了订购流程的概述。

The sample e-commerce application that we look at from time to time needs to be able to receive orders. This is often best done by a separate application or subsystem because, at that point, the semantics of the transaction change. As long as you’re looking at a shopping basket, you can dynamically calculate unit prices, exchange rates, and discounts. But when a customer places an order, all of those values must be captured and frozen as they were presented when the customer approved the order. Table 6.1 provides an overview of the order process.

表 6.1 当订单子系统批准订单时,它必须执行许多不同的操作。
行动必需的依赖项
更新订单IOrderRepository
向客户发送收据电子邮件IMessageService
将发票金额通知会计系统IBillingSystem
根据购买的物品和与送货地址的接近程度,选择最佳仓库来挑选和运送订单ILocationService,IInventoryManagement
要求选定的仓库挑选并运送整个订单或部分订单IInventoryManagement

仅批准一个订单就需要五个不同的依赖项。想象一下您需要处理其他与订单相关的操作的其他依赖项!

Five different Dependencies are required just to approve an order. Imagine the other Dependencies you’d need to handle other order-related operations!

让我们回顾一下如果消费OrderService类直接导入所有这些Dependencies 会是什么样子。下面的清单给出了这个类的内部结构的快速概览。

Let’s review how this would look if the consuming OrderService class directly imported all of these Dependencies. The following listing gives a quick overview of the internals of this class.

气味.tif

清单 6.2 原始OrderService有很多依赖

Listing 6.2 Original OrderService class with many Dependencies

public class OrderService : IOrderService
{
    private readonly IOrderRepository orderRepository;
    private readonly IMessageService messageService;
    private readonly IBillingSystem billingSystem;
    private readonly ILocationService locationService;
    private readonly IInventoryManagement inventoryManagement;

    public OrderService(
        IOrderRepository orderRepository,
        IMessageService messageService,
        IBillingSystem billingSystem,
        ILocationService locationService,
        IInventoryManagement inventoryManagement)
    {
        this.orderRepository = orderRepository;
        this.messageService = messageService;
        this.billingSystem = billingSystem;
        this.locationService = locationService;
        this.inventoryManagement = inventoryManagement;
    }

    public void ApproveOrder(Order order)
    {
        this.UpdateOrder(order);    ①  
        this.Notify(order);    ②  
    }

    private void UpdateOrder(Order order)
    {
        order.Approve();
        this.orderRepository.Save(order);
    }

    private void Notify(Order order)
    {
        this.messageService.SendReceipt(new OrderReceipt { ... });
        this.billingSystem.NotifyAccounting(...);
        this.Fulfill(order);
    }

    private void Fulfill(Order order)
    {
        this.locationService.FindWarehouses(...);    ③  
        this.inventoryManagement.NotifyWarehouses(...);  ④  
    }
}

为了使示例易于管理,我们省略了该类的大部分细节。但是不难想象这样一个类相当庞大和复杂。如果你让OrderService直接消耗​​所有五个Dependencies,你会得到很多细粒度的Dependencies。结构如图6.1所示。

To keep the example manageable, we omitted most of the details of the class. But it’s not hard to imagine such a class to be rather large and complex. If you let OrderService directly consume all five Dependencies, you get many fine-grained Dependencies. The structure is shown in figure 6.1.

06-01.eps

图 6.1 OrderService有五个直接依赖关系,这表明违反了 SRP。

Figure 6.1 OrderService has five direct Dependencies, which suggests an SRP violation.

如果您对类使用构造函数注入OrderService(你应该这样做),你有一个带有五个参数的构造函数。这太多了,表明OrderService有太多的责任。另一方面,所有这些依赖项都是必需的,因为OrderService该类在收到新订单时必须实现所有所需的功能。OrderService您可以通过使用 Facade Services 重构进行重新设计来解决这个问题。我们将在下一节中向您展示如何操作。

If you use Constructor Injection for the OrderService class (which you should), you have a constructor with five parameters. This is too many and indicates that OrderService has too many responsibilities. On the other hand, all of these Dependencies are required because the OrderService class must implement all of the desired functionality when it receives a new order. You can address this issue by redesigning OrderService using Facade Services refactoring. We’ll show you how to do that in the next section.

6.1.2 从构造函数过度注入到门面服务的重构

6.1.2 Refactoring from Constructor Over-injection to Facade Services

重新设计时OrderService,您需要做的第一件事是寻找自然的交互集群。和之间的交互应立即引起您的注意,因为您使用它们来查找可以完成订单的最近仓库。这可能是一个复杂的算法。ILocationServiceIInventoryManagement

When redesigning OrderService, the first thing you need to do is to look for natural clusters of interaction. The interaction between ILocationService and IInventoryManagement should immediately draw your attention, because you use them to find the closest warehouses that can fulfill the order. This could potentially be a complex algorithm.

选择仓库后,您需要将订单通知他们。如果您进一步考虑一下,这ILocationService是一个将订单通知相应仓库的实施细节。整个交互可以隐藏在IOrderFulfillment界面后面, 像这样:

After you’ve selected the warehouses, you need to notify them about the order. If you think about this a little further, ILocationService is an implementation detail of notifying the appropriate warehouses about the order. The entire interaction can be hidden behind an IOrderFulfillment interface, like this:

public interface IOrderFulfillment
{
    void Fulfill(Order order);
}

下一个清单显示了新IOrderFulfillment接口的实现.

The next listing shows the implementation of the new IOrderFulfillment interface.

好的.tif

清单 6.3 OrderFulfillment

Listing 6.3 OrderFulfillment class

public class OrderFulfillment : IOrderFulfillment
{
    private readonly ILocationService locationService;
    private readonly IInventoryManagement inventoryManagement;

    public OrderFulfillment(
        ILocationService locationService,
        IInventoryManagement inventoryManagement)
    {
        this.locationService = locationService;
        this.inventoryManagement = inventoryManagement;
    }

    public void Fulfill(Order order)
    {
        this.locationService.FindWarehouses(...);
        this.inventoryManagement.NotifyWarehouses(...);
    }
}

有趣的是,订单履行本身听起来很像一个领域概念。您可能发现了一个隐含的域概念并将其显式化。

Interestingly, order fulfillment sounds a lot like a domain concept in its own right. Chances are that you discovered an implicit domain concept and made it explicit.

的默认实现IOrderFulfillment消耗了两个原始的Dependencies,所以它有一个带两个参数的构造函数,这很好。作为进一步的好处,您已将用于为给定订单查找最佳仓库的算法封装到可重用组件中。新的IOrderFulfillment 抽象是一个外观服务,因为它隐藏了两个交互的依赖及其行为。

The default implementation of IOrderFulfillment consumes the two original Dependencies, so it has a constructor with two parameters, which is fine. As a further benefit, you’ve encapsulated the algorithm for finding the best warehouse for a given order into a reusable component. The new IOrderFulfillment Abstraction is a Facade Service because it hides the two interacting Dependencies with their behavior.

此重构将两个依赖项合并为一个,但在类上留下了四个依赖项OrderService如图6.2所示。您还需要寻找其他机会将Dependencies聚合到 Facade 中。

This refactoring merges two Dependencies into one but leaves you with four Dependencies on the OrderService class, as shown in figure 6.2. You also need to look for other opportunities to aggregate Dependencies into a Facade.

该类OrderService只有四个DependenciesOrderFulfillment该类包含两个。这不是一个糟糕的开始,但您可以OrderService进一步简化。接下来您可能会注意到,所有要求都涉及将订单通知其他系统。这表明您可以定义一个对通知进行建模的通用抽象,也许是这样的:

The OrderService class only has four Dependencies, and the OrderFulfillment class contains two. That’s not a bad start, but you can simplify OrderService even more. The next thing you may notice is that all the requirements involve notifying other systems about the order. This suggests that you can define a common Abstraction that models notifications, perhaps something like this:

public interface INotificationService
{
    void OrderApproved(Order order);
}
06-02.eps

图 6.2聚合后的 两个依赖OrderServiceFacade Service

Figure 6.2 Two Dependencies of OrderService aggregated behind a Facade Service

可以使用此接口实现对外部系统的每个通知。但是您可能想知道这有什么帮助,因为您已经将每个依赖项包装在一个新接口中。Dependencies的数量没有减少,所以你有什么收获吗?

Each notification to an external system can be implemented using this interface. But you may wonder how this helps, because you’ve wrapped each Dependency in a new interface. The number of Dependencies didn’t decrease, so did you gain anything?

是的,你做到了。因为所有三个通知都实现相同的接口,所以您可以将它们包装在 Composite 6  模式中清单 6.4所示。这显示了另一个实现,它包装了一个实例集合并在所有这些实例上调用该方法。INotificationServiceINotificationServiceOrderAccepted

Yes, you did. Because all three notifications implement the same interface, you can wrap them in a Composite6  pattern as can be seen in listing 6.4. This shows another implementation of INotificationService that wraps a collection of INotificationService instances and invokes the OrderAccepted method on all of those.

好的.tif

清单 6.4 复合包装INotificationService实例

Listing 6.4 Composite wrapping INotificationService instances

public class CompositeNotificationService
    : INotificationService    ①  
{
    IEnumerable<INotificationService> services;

    public CompositeNotificationService(
        IEnumerable<INotificationService> services)  ②  
    {
        this.services = services;
    }

    public void OrderApproved(Order order)
    {
        foreach (var service in this.services)
        {
            service.OrderApproved(order);    ③  
        }
    }
}

CompositeNotificationService实现并将传入调用转发到其包装的实现。这可以防止消费者不得不处理多个实现,这是一个实现细节。这意味着您可以让depend on a single ,它只留下两个Dependencies,如下所示。INotificationServiceOrderServiceINotificationService

CompositeNotificationService implements INotificationService and forwards an incoming call to its wrapped implementations. This prevents the consumer from having to deal with multiple implementations, which is an implementation detail. This means that you can let OrderService depend on a single INotificationService, which leaves just two Dependencies, as shown next.

好的.tif

清单 6.5OrderService用两个依赖 重构

Listing 6.5 Refactored OrderService with two Dependencies

public class OrderService : IOrderService
{
    private readonly IOrderRepository orderRepository;
    private readonly INotificationService notificationService;

    public OrderService(
        IOrderRepository orderRepository,
        INotificationService notificationService)
    {
        this.orderRepository = orderRepository;
        this.notificationService = notificationService;
    }

    public void ApproveOrder(Order order)
    {
        this.UpdateOrder(order);

        this.notificationService.OrderApproved(order);
    }

    private void UpdateOrder(Order order)
    {
        order.Approve();
        this.orderRepository.Save(order);
    }
}

从概念的角度来看,这也是有道理的。在高层次上,您不需要关心如何OrderService通知其他系统的细节,但您确实关心它所做的事情。这减少OrderService到只有两个Dependencies,这是一个更合理的数字。

From a conceptual perspective, this also makes sense. At a high level, you don’t need to care about the details of how OrderService notifies other systems, but you do care that it does. This reduces OrderService to only two Dependencies, which is a more reasonable number.

从消费者的角度来看,OrderService在功能上没有变化,这使它成为真正的重构。另一方面,在概念层面上,OrderService发生了变化。它现在的职责是接收订单、保存订单并通知其他系统。通知哪些系统以及如何实施的细节已被推到更详细的级别。图 6.3显示了最终的DependenciesOrderService

From the consumer’s perspective, OrderService is functionally unchanged, making this a true refactoring. On the other hand, on the conceptual level, OrderService is changed. Its responsibility is now to receive an order, save it, and notify other systems. The details of which systems are notified and how this is implemented have been pushed down to a more detailed level. Figure 6.3 shows the final Dependencies of OrderService.

06-03.eps

图 6.3OrderService重构后 的Dependencies

Figure 6.3 The final OrderService with refactored Dependencies

使用,您现在可以创建及其依赖项CompositeNotificationServiceOrderService

Using the CompositeNotificationService, you can now create the OrderService with its Dependencies.

清单 6.6 使用 Facade Services 重构的Composition Root

Listing 6.6 Composition Root refactored using Facade Services

var repository = new SqlOrderRepository(connectionString);

var notificationService = new CompositeNotificationService(
    new INotificationService[]
    {
        new OrderApprovedReceiptSender(messageService),
        new AccountingNotifier(billingSystem),
        new OrderFulfillment(locationService, inventoryManagement)
    });

var orderServive = new OrderService(repository, notificationService);

即使您始终使用构造函数注入,也没有一个类的构造函数最终需要两个以上的参数。将 an作为单个参数。CompositeNotificationServiceIEnumerable<INotificationService>

Even though you consistently use Constructor Injection throughout, no single class’s constructor ends up requiring more than two parameters. CompositeNotificationService takes an IEnumerable<INotificationService> as a single argument.

一个有益的副作用是,发现这些自然集群会将以前未发现的关系和领域概念公开。在此过程中,您将隐式概念转变为显式概念。7  每个聚合都变成了在更高级别捕获此交互的服务,而消费者的唯一职责就是协调这些更高级别的服务。如果您有一个复杂的应用程序,其中消费者最终对 Facade Services有太多的依赖,您可以重复这个重构。创建 Facade Services 的 Facade Service 是一件非常明智的事情。

A beneficial side effect is that discovering these natural clusters draws previously undiscovered relationships and domain concepts out into the open. In the process, you turn implicit concepts into explicit concepts.7  Each aggregate becomes a service that captures this interaction at a higher level, and the consumer’s single responsibility becomes to orchestrate these higher-level services. You can repeat this refactoring if you have a complex application where the consumer ends up with too many Dependencies on Facade Services. Creating a Facade Service of Facade Services is a perfectly sensible thing to do.

Facade Services 重构是处理系统复杂性的好方法。但是关于这个OrderService例子,我们甚至可以更进一步,将我们带到领域事件。

The Facade Services refactoring is a great way to handle complexity in a system. But with regard to the OrderService example, we might even take this one step further, bringing us to domain events.

6.1.3 从构造函数过度注入到领域事件的重构

6.1.3 Refactoring from Constructor Over-injection to domain events

清单 6.5显示所有通知都是在订单被批准时触发的操作。下面的代码再次显示了这个相关部分:

Listing 6.5 shows that all notifications are actions triggered when an order is approved. The following code shows this relevant part again:

this.notificationService.OrderApproved(order);

可以说,一个订单被批准的行为对业务来说是很重要的。这些类型的事件称为领域事件,并且在您的应用程序中更明确地对它们建模可能很有价值。

We can say that the act of an order being approved is of importance to the business. These kinds of events are called domain events, and it might be valuable to model them more explicitly in your applications.

的引入虽然是一个很大的改进,但也只是解决了和它直接Dependencies级别的问题。当将相同的重构技术应用到系统中的其他类时,可以很容易地想象如何演变为类似于以下列表的东西。INotificationServiceOrderServiceOrderServiceINotificationService

Although the introduction of INotificationService is a great improvement to OrderService, it only solves the problem at the level of OrderService and its direct Dependencies. When applying the same refactoring technique to other classes in the system, one could easily imagine how INotificationService evolves toward something similar to the following listing.

坏.tif

清单 6.7 INotificationService中有越来越多的方法

Listing 6.7 INotificationService with a growing number of methods

public interface INotificationService
{
    void OrderApproved(Order order);    ①  
    void OrderCancelled(Order order);    ①  
    void OrderShipped(Order order);    ①  
    void OrderDelivered(Order order);    ①  
    void CustomerCreated(Customer customer);    ①  
    void CustomerMadePreferred(Customer customer);    ①  
}

在任何具有合理规模和复杂性的系统中,您很容易获得数十个这样的域事件,这将导致不断变化的INotificationService界面。随着对该接口的每次更改,该接口的所有实现也必须更新。此外,不断增长的接口也会导致不断增长的实现。但是,如果您将领域事件提升为实际类型并使它们成为领域的一部分,如图 6.4所示,那么将出现一个有趣的进一步概括的机会。

Within any system of reasonable size and complexity, you’d easily get dozens of these domain events, which would lead to an ever-changing INotificationService interface. With each change to this interface, all implementations of that interface must be updated too. Additionally, ever-growing interfaces also causes ever-growing implementations. If, however, you promote the domain events to actual types and make them part of the domain, as shown in figure 6.4, an interesting opportunity to generalize even further arises.

06-04.eps

图 6.4 提升为实际类型的领域事件。这些类型只包含数据,不包含行为。

Figure 6.4 Domain events promoted to actual types. These types contain only data and no behavior.

下面的清单显示了图 6.4中说明的域事件代码。

The following listing shows the domain event code illustrated in figure 6.4.

好的.tif

清单 6.8 和域事件类OrderApprovedOrderCancelled

Listing 6.8 OrderApproved and OrderCancelled domain event classes

public class OrderApproved
{
    public readonly Guid OrderId;

    public OrderApproved(Guid orderId)
    {
        this.OrderId = orderId;
    }
}

public class OrderCancelled
{
    public readonly Guid OrderId;

    public OrderCancelled(Guid orderId)
    {
        this.OrderId = orderId;
    }
}

尽管OrderApprovedOrderCancelled类都具有相同的结构并且与相同的Entity相关,但是围绕它们自己的类对它们进行建模可以更轻松地创建响应此类特定事件的代码。当系统中的每个域事件都获得自己的类型时,它允许您INotificationService使用单个方法更改为通用接口,如以下清单所示。

Although both the OrderApproved and OrderCancelled classes have the same structure and are related to the same Entity, modelling them around their own class makes it easier to create code that responds to such a specific event. When each domain event in your system gets its own type, it lets you change INotificationService to a generic interface with a single method, as the following listing shows.

好的.tif

清单 6.9IEventHandler<TEvent>只有一个方法的 泛型

Listing 6.9 Generic IEventHandler<TEvent> with just a single method

public interface IEventHandler<TEvent>    ①  
{
    void Handle(TEvent e);
}

在 的情况下IEventHandler<TEvent>,从接口派生的类必须指定TEvent类型— 例如OrderCancelled— 在类声明中。然后,此类型将用作该类Handle方法的参数类型。这允许一个接口统一多个类,尽管它们的类型不同。此外,它允许这些实现中的每一个都是强类型的,专门针对指定为TEvent.

In the case of IEventHandler<TEvent>, a class deriving from the interface must specify a TEvent type — for the instance OrderCancelled — in the class declaration. This type will then be used as the parameter type for that class’s Handle method. This allows one interface to unify several classes, despite differences in their types. In addition, it allows each of those implementations to be strongly typed, working exclusively off whatever type was specified as TEvent.

基于此接口,您现在可以构建响应域事件的类,例如OrderFulfillment你以前看过。基于新IEventHandler<TEvent>界面,原始OrderFulfillment类,如清单 6.3所示,更改为以下清单中显示的类。

Based on this interface, you can now build the classes that respond to a domain event, like the OrderFulfillment class you saw previously. Based on the new IEventHandler<TEvent> interface, the original OrderFulfillment class, as shown in listing 6.3, changes to that displayed in the following listing.

好的.tif

清单 6.10 OrderFulfillment类实现IEventHandler<TEvent>

Listing 6.10 OrderFulfillment class implementing IEventHandler<TEvent>

public class OrderFulfillment
    : IEventHandler<OrderApproved>    ①  
{
    private readonly ILocationService locationService;
    private readonly IInventoryManagement inventoryManagement;

    public OrderFulfillment(
        ILocationService locationService,
        IInventoryManagement inventoryManagement)
    {
        this.locationService = locationService;
        this.inventoryManagement = inventoryManagement;
    }

    public void Handle(OrderApproved e)    ②  
    {
        this.locationService.FindWarehouses(...);
        this.inventoryManagement.NotifyWarehouses(...);
    }
}

OrderFulfillment班级_implements IEventHandler<OrderApproved>,意味着它作用于OrderApproved事件。OrderService然后使用新IEventHandler<TEvent>界面如图 6.5所示。

The OrderFulfillment class implements IEventHandler<OrderApproved>, meaning that it acts on OrderApproved events. OrderService then uses the new IEventHandler<TEvent> interface, as figure 6.5 shows.

06-05.eps

图 6.5 该类OrderService依赖于一个IEventHandler<OrderApproved>接口,而不是INotificationService.

Figure 6.5 The OrderService class depends on an IEventHandler<OrderApproved> interface, instead of INotificationService.

清单 6.11显示了一个OrderService取决于IEventHandler<OrderApproved>. 与清单 6.5相比,OrderService逻辑几乎保持不变。

Listing 6.11 shows an OrderService depending on IEventHandler<OrderApproved>. Compared to listing 6.5, the OrderService logic will stay almost unchanged.

好的.tif

清单 6.11 OrderService取决于IEventHandler<OrderApproved>

Listing 6.11 OrderService depending on IEventHandler<OrderApproved>

public class OrderService : IOrderService
{
    private readonly IOrderRepository orderRepository;
    private readonly IEventHandler<OrderApproved> handler;

    public OrderService(
        IOrderRepository orderRepository,
        IEventHandler<OrderApproved> handler)    ①  
    {
        this.orderRepository = orderRepository;
        this.handler = handler;
    }

    public void ApproveOrder(Order order)
    {
        this.UpdateOrder(order);

        this.handler.Handle(
            new OrderApproved(order.Id));    ②  
    }
    ...
}

与非泛型一样INotificationService,您仍然需要一个 Composite 负责将信息分派到可用处理程序列表。这使您能够向应用程序添加新的处理程序,而无需更改OrderService. 清单 6.12显示了这个组合。如您所见,它类似于清单 6.4中的代码。CompositeNotificationService

Just as with the non-generic INotificationService, you still need a Composite that takes care of dispatching the information to the list of available handlers. This enables you to add new handlers to the application, without the need to change OrderService. Listing 6.12 shows this Composite. As you can see, it’s similar to the CompositeNotificationService from listing 6.4.

清单 6.12 复合包装IEventHandler<TEvent>实例

Listing 6.12 Composite wrapping IEventHandler<TEvent> instances

public class CompositeEventHandler<TEvent> : IEventHandler<TEvent>
{
    private readonly IEnumerable<IEventHandler<TEvent>> handlers;

    public CompositeEventHandler(
        IEnumerable<IEventHandler<TEvent>> handlers)    ①  
    {
        this.handlers = handlers;
    }

    public void Handle(TEvent e)
    {
        foreach (var handler in this.handlers)
        {
            handler.Handle(e);
        }
    }
}

IEventHandler<TEvent>像 那样包装实例集合CompositeEventHandler<TEvent>,让您可以向系统添加任意事件处理程序实现,而无需对IEventHandler<TEvent>. 使用 new CompositeEventHandler<TEvent>,您可以创建OrderService及其依赖项

Wrapping a collection of IEventHandler<TEvent> instances, as does CompositeEventHandler<TEvent>, lets you add arbitrary event handler implementations to the system without having to make any changes to consumers of IEventHandler<TEvent>. Using the new CompositeEventHandler<TEvent>, you can create the OrderService with its Dependencies.

清单 6.13 重构的 using 事件的组合根OrderService

Listing 6.13 Composition Root for the OrderService refactored using events

var orderRepository = new SqlOrderRepository(connectionString);

var orderApprovedHandler = new CompositeEventHandler<OrderApproved>(
    new IEventHandler<OrderApproved>[]
    {
        new OrderApprovedReceiptSender(messageService),
        new AccountingNotifier(billingSystem),
        new OrderFulfillment(locationService, inventoryManagement)
    });

var orderService = new OrderService(orderRepository, orderApprovedHandler);

同样,组合根将包含其他域事件处理程序的配置。OrderCancelled以下代码显示了和的更多事件处理程序CustomerCreated。我们留给读者从中推断。

Likewise, the Composition Root will contain the configuration for the handlers of other domain events. The following code shows a few more event handlers for OrderCancelled and CustomerCreated. We leave it up to the reader to extrapolate from this.

var orderCancelledHandler = new CompositeEventHandler<OrderCancelled>(
    new IEventHandler<OrderCancelled>[]
    {
        new AccountingNotifier(billingSystem),
        new RefundSender(orderRepository),
    });

var customerCreatedHandler = new CompositeEventHandler<CustomerCreated>(
    new IEventHandler<CustomerCreated>[]
    {
        new CrmNotifier(crmSystem),
        new TermsAndConditionsSender(messageService, termsRepository),
    });

var orderService = new OrderService(
    orderRepository, orderApprovedHandler, orderCancelledHandler);

var customerService = new CustomerService(
    customerRepository, customerCreatedHandler);

像这样的通用接口的美妙之处IEventHandler<TEvent>在于,添加新功能不会对接口或任何已经存在的实现造成任何更改。如果您需要为批准的订单生成发票,您只需添加一个新的实现,实现IEventHandler<OrderApproved>. 创建新的领域事件时,不需要进行任何更改CompositeEventHandler<TEvent>

The beauty of a generic interface like IEventHandler<TEvent> is that the addition of new features won’t cause any changes to either the interface nor any of the already existing implementations. In case you need to generate an invoice for your approved order, you only have to add a new implementation that implements IEventHandler<OrderApproved>. When a new domain event is created, no changes to CompositeEventHandler<TEvent> are required.

从某种意义上说,IEventHandler<TEvent>成为应用程序所依赖的通用构建块的模板。每个构建块响应特定事件。如您所见,您可以有多个构建块来响应同一事件。无需更改任何现有业务逻辑即可插入新的构建块。

In a sense, IEventHandler<TEvent> becomes a template for common building blocks that the application relies on. Each building block responds to a particular event. As you saw, you can have multiple building blocks that respond to the same event. New building blocks can be plugged in without the need to change any existing business logic.

尽管 的引入IEventHandler<TEvent>阻止了不断增长的问题,但并不能阻止不断增长的类的问题。这是我们将在第 10 章中详细讨论的内容。INotificationServiceOrderService

Although the introduction of IEventHandler<TEvent> prevented the problem of an ever-growing INotificationService, it doesn’t prevent the problem of an ever-growing OrderService class. This is something we’ll address in great detail in chapter 10.

我们发现使用域事件是一种有效的模型。它允许在更概念化的层面上定义代码,同时让您构建更健壮的软件,尤其是在您必须与不属于数据库事务的外部系统进行通信的情况下。但无论您选择哪种重构方法,无论是装饰器、门面服务、领域事件还是其他重构方法,这里的重要收获是构造函数过度注入是代码异味的明显标志。不要忽视这样的迹象,而是采取相应的行动。

We’ve found the use of domain events to be an effective model. It allows code to be defined on a more conceptual level, while letting you build more-robust software, especially where you have to communicate with external systems that aren’t part of your database transaction. But no matter which refactoring approach you choose, be it Decorators, Facade Services, domain events, or perhaps another, the important takeaway here is that Constructor Over-injection is a clear sign that code smells. Don’t ignore such a sign, but act accordingly.

由于构造函数过度注入是一种常见的重复代码异味,下一节将讨论一个更微妙的问题,乍一看,它可能是解决一组重复问题的好方法。但是吗?

Because Constructor Over-injection is a commonly recurring code smell, the next section discusses a more subtle problem that, at first sight, might look like a good solution to a set of recurring problems. But is it?

6.2 滥用抽象工厂

6.2 Abuse of Abstract Factories

当您开始应用 DI 时,您可能遇到的第一个困难是抽象依赖于运行时值。例如,在线地图站点可能会提供计算两个位置之间的路线的功能,让您可以选择路线的计算方式。你想要最短路线吗?基于已知交通模式的最快路线?风景最美的路线?

When you start applying DI, one of the first difficulties you’re likely to encounter is when Abstractions depend on runtime values. For example, an online mapping site may offer to calculate a route between two locations, giving you a choice of how you want the route computed. Do you want the shortest route? The fastest route based on known traffic patterns? The most scenic route?

在这种情况下,许多开发人员的第一反应是使用抽象工厂。尽管抽象工厂在软件中确实占有一席之地,但当涉及到 DI 时——当工厂被用作应用程序组件中的依赖项时——它们经常被过度使用。在许多情况下,存在更好的选择。

The first response from many developers in such cases would be to use an Abstract Factory. Although Abstract Factories do have their place in software, when it comes to DI — when factories are used as DEPENDENCIES in application components — they're often overused. In many cases, better alternatives exist.

在本节中,我们将讨论存在抽象工厂更好替代方案的两种情况。在第一种情况下,我们将讨论为什么不应该使用抽象工厂来创建生命周期较短的有状态依赖项。之后,我们将讨论为什么通常最好不要使用抽象工厂来根据运行时数据选择依赖项。

In this section, we’ll discuss two cases where better alternatives to Abstract Factories exist. In the first case, we’ll discuss why Abstract Factories shouldn’t be used to create stateful Dependencies with a short lifetime. After that, we’ll discuss why it’s generally better not to use Abstract Factories to select Dependencies based on runtime data.

6.2.1 滥用抽象工厂解决生命周期问题

6.2.1 Abusing Abstract Factories to overcome lifetime problems

当谈到滥用抽象工厂时,一种常见的代码味道是看到无参数工厂方法将Dependency作为返回类型,如下一个清单所示。

When it comes to the abuse of Abstract Factories, a common code smell is to see parameterless factory methods that have a Dependency as the return type, as the next listing shows.

气味.tif

清单 6.14Create具有无参数方法 的抽象工厂

Listing 6.14 Abstract Factory with parameterless Create method

public interface IProductRepositoryFactory
{
    IProductRepository Create();    ①  
}

具有无参数方法的抽象工厂Create通常用于允许消费者控制其依赖项的生命周期。在下面的清单中,通过向工厂请求它来HomeController控制它的生命周期IProductRepository,并在它使用完后将其丢弃。

Abstract Factories with parameterless Create methods are often used to allow consumers to control the lifetime of their Dependencies. In the following listing, HomeController controls the lifetime of IProductRepository by requesting it from the factory, and disposing of it when it finishes using it.

气味.tif

清单 6.15 A显式管理其Dependency的生命周期HomeController

Listing 6.15 A HomeController explicitly managing its Dependency’s lifetime

public class HomeController : Controller
{
    private readonly IProductRepositoryFactory factory;

    public HomeController(
        IProductRepositoryFactory factory)    ①  
    {
        this.factory = factory;
    }

    public ViewResult Index()
    {
        using (IProductRepository repository =
            this.factory.Create())    ②  
        {
            var products =
                repository.GetFeaturedProducts();    ③  

            return this.View(products);
        }    ④  
    }
}

图 6.6HomeController显示了它和它的Dependencies之间的通信顺序。

Figure 6.6 shows the sequence of communication between HomeController and its Dependencies.

06-06.eps

图 6.6 消费类控制其Dependency的生命周期。它通过从Dependency请求 Repository 实例并在完成时调用该实例来实现。HomeControllerIProductRepositoryIProductRepositoryFactoryDisposeIProductRepository

Figure 6.6 The consuming class HomeController controls the lifetime of its IProductRepositoryDependency. It does so by requesting a Repository instance from the IProductRepositoryFactoryDependency and calling Dispose on the IProductRepository instance when it’s done with it.

当使用的实现持有应该以确定性方式关闭的资源(例如数据库连接)时,需要处置存储库。尽管实现可能需要确定性清理,但这并不意味着消费者有责任确保正确清理。这给我们带来了抽象泄漏的概念。

Disposing the Repository is required when the used implementation holds on to resources, such as database connections, that should be closed in a deterministic fashion. Although an implementation might require deterministic cleanup, that doesn’t imply that it should be the responsibility of the consumer to ensure proper cleanup. This brings us to the concept of Leaky Abstractions.

有漏洞的抽象代码味道

The Leaky Abstraction code smell

就像测试驱动开发一样(测试驱动开发) 确保可测性,最安全的做法是先定义接口,然后再针对它们进行编程。即便如此,在某些情况下,您已经有了一个具体类型,现在想要提取一个接口。执行此操作时,必须注意不要泄漏底层实现。发生这种情况的一种方式是,如果您只从给定的具体类型中提取接口,但某些参数或返回类型仍然是您要从中抽象的库中定义的具体类型。以下接口定义提供了一个示例:

Just as Test-Driven Development (TDD) ensures Testability, it’s safest to define interfaces first and then subsequently program against them. Even so, there are cases where you already have a concrete type and now want to extract an interface. When you do this, you must take care that the underlying implementation doesn’t leak through. One way this can happen is if you only extract an interface from a given concrete type, but some of the parameter or return types are still concrete types defined in the library you want to abstract from. The following interface definition offers an example:

气味.tif

public interface IRequestContext    ①  
{
    HttpContext Context { get; }    ②  
}

如果需要提取接口,则需要以递归的方式进行,确保根接口公开的所有类型本身都是接口。我们称之为Deep Extraction,结果是Deep Interfaces

If you need to extract an interface, you need to do it in a recursive manner, ensuring that all types exposed by the root interface are themselves interfaces. We call this Deep Extraction, and the result is Deep Interfaces.

这并不意味着接口不能公开任何具体类。公开无行为的数据对象通常很好,例如参数对象、视图模型和数据传输对象 (DTO)). 它们定义在与接口相同的库中,而不是您要从中抽象的库。这些数据对象是抽象的一部分。

This doesn’t mean that interfaces can’t expose any concrete classes. It’s typically fine to expose behaviorless data objects, such as Parameter Objects, view models, and Data Transfer Objects (DTOs). They’re defined in the same library as the interface instead of the library you want to abstract from. Those data objects are part of the Abstraction.

小心深度提取:它并不总能带来最佳解决方案。以前面的例子为例。考虑以下深度提取IHttpContext接口的可疑实现:

Be careful with Deep Extraction: it doesn’t always lead to the best solution. Take the previous example. Consider the following suspicious-looking implementation of a Deep Extracted IHttpContext interface:

气味.tif

public interface IHttpContext    ①  
{
    IHttpRequest Request { get; }    ②  
    IHttpResponse Response { get; }    ②  
    IHttpSession Session { get; }    ②  
    IPrincipal User { get; }    ②  
}

尽管您可能一直在使用接口,但很明显 HTTP 模型正在泄漏。换句话说,它仍然是一个有漏洞的抽象——它的子接口也是如此。IHttpContext

Although you might be using interfaces all the way down, it’s still glaringly obvious that the HTTP model is leaking through. In other words, IHttpContext is still a Leaky Abstraction — and so are its sub-interfaces.

你应该如何建模IRequestContext呢?要弄清楚这一点,您必须了解其消费者想要实现的目标。例如,如果消费者需要找出发送当前 Web 请求的用户的角色,您最终可能会使用我们在第 3 章中讨论的方法:IUserContext

How should you model IRequestContext instead? To figure this out, you have to look at what its consumers want to achieve. For instance, if a consumer needs to find out the role of the user who sent the current web request, you might end up instead with the IUserContext we discussed in chapter 3:

好的.tif

public interface IUserContext
{
    bool IsInRole(Role role);
}

这个IUserContext界面不会向消费者透露它作为 ASP.NET Web 应用程序的一部分运行。事实上,此抽象允许您将相同的使用者作为 Windows 服务或桌面应用程序的一部分运行。它可能需要创建一个不同的IUserContext实现,但它的消费者并没有注意到这一点。

This IUserContext interface doesn’t reveal to the consumer that it’s running as part of an ASP.NET web application. As a matter of fact, this Abstraction lets you run the same consumer as part of a Windows service or desktop application. It’ll likely require the creation of a different IUserContext implementation, but its consumers are oblivious to this.

始终考虑给定的抽象对于您所想到的实现之外的实现是否有意义。如果没有,您应该重新考虑您的设计。这让我们回到我们的无参数工厂方法。

Always consider whether a given Abstraction makes sense for implementations other than the one you have in mind. If it doesn’t, you should reconsider your design. That brings us back to our parameterless factory methods.

无参数工厂方法是Leaky Abstractions

Parameterless factory methods are Leaky Abstractions

尽管抽象工厂模式很有用,但您必须小心谨慎地应用它。抽象工厂创建的依赖项在概念上应该需要一个运行时值,并且从运行时值到抽象的转换应该是有意义的。如果您因为心中有一个特定的实现而感到引入抽象工厂的冲动,那么您可能手边有一个Leaky Abstraction

As useful as the Abstract Factory pattern can be, you must take care to apply it with discrimination. The Dependencies created by an Abstract Factory should conceptually require a runtime value, and the translation from a runtime value into an Abstraction should make sense. If you feel the urge to introduce an Abstract Factory because you have a specific implementation in mind, you may have a Leaky Abstraction at hand.

依赖于 的消费者,例如清单 6.15中的消费者,不应该关心他们获得了哪个实例。在运行时,您可能需要创建多个实例,但就消费者而言,只有一个。IProductRepositoryHomeController

Consumers that depend on IProductRepository, such as the HomeController from listing 6.15, shouldn’t care about which instance they get. At runtime, you might need to create multiple instances, but as far as the consumer is concerned, there’s only one.

通过使用无参数 方法指定抽象IProductRepositoryFactory Create,你让消费者知道给定服务有更多的实例,并且它必须处理这个。由于 的另一个实现IProductRepository可能根本不需要多个实例或确定性处理,因此您通过抽象工厂及其无参数Create方法泄漏了实现细节。换句话说,您已经创建了一个Leaky Abstraction

By specifying an IProductRepositoryFactory Abstraction with a parameterless Create method, you let the consumer know that there are more instances of the given service, and that it has to deal with this. Because another implementation of IProductRepository might not require multiple instances or deterministic disposal at all, you’re therefore leaking implementation details through the Abstract Factory with its parameterless Create method. In other words, you’ve created a Leaky Abstraction.

接下来,我们将讨论如何防止这种Leaky Abstraction代码异味。

Next, we’ll discuss how to prevent this Leaky Abstraction code smell.

重构以获得更好的解决方案

Refactoring toward a better solution

使用代码不应该关心存在多个代码的可能性IProductRepository实例。因此,您应该摆脱IProductRepositoryFactory完全而不是让消费者完全依赖IProductRepository,他们应该使用Constructor Injection 注入。此建议反映​​在以下列表中。

Consuming code shouldn’t be concerned with the possibility of there being more than one IProductRepository instance. You should therefore get rid of the IProductRepositoryFactory completely and instead let consumers depend solely on IProductRepository, which they should have injected using Constructor Injection. This advice is reflected in the following listing.

好的.tif

清单 6.16 HomeController没有管理其依赖的生命周期

Listing 6.16 HomeController without managing its Dependency’s lifetime

public class HomeController : Controller
{
    private readonly IProductRepository repository;

    public HomeController(
        IProductRepository repository)    ①  
    {
        this.repository = repository;
    }

    public ViewResult Index()
    {
        var products =    ②  
            this.repository.GetFeaturedProducts();    ②  
    ②  
        return this.View(products);    ②  
    }
}

这段代码产生了一个简化的交互序列HomeController和它唯一的IProductRepository 依赖关系如图 6.7所示。

This code results in a simplified sequence of interactions between HomeController and its sole IProductRepository Dependency, as shown in figure 6.7.

06-07.eps

图 6.7图 6.6相比,移除管理IProductRepository生命周期的责任以及移除IProductRepositoryFactory依赖项大大简化了与依赖HomeController项的交互。

Figure 6.7 Compared to figure 6.6, removing the responsibility of managing IProductRepository’s lifetime together with removing the IProductRepositoryFactoryDependency considerably simplifies interaction with HomeController’s Dependencies.

尽管删除了终身管理简化HomeController,您必须在应用程序的某处管理存储库的生命周期。解决此问题的常见模式是代理模式,下一个清单中给出了一个示例。

Although removing Lifetime Management simplifies the HomeController, you’ll have to manage the Repository’s lifetime somewhere in the application. A common pattern to address this problem is the Proxy pattern, an example of which is given in the next listing.

好的.tif

清单 6.17使用代理 延迟创建SqlProductRepository

Listing 6.17 Delaying creation of SqlProductRepository using a Proxy

public class SqlProductRepositoryProxy : IProductRepository
{
    private readonly string connectionString;

    public SqlProductRepositoryProxy(string connectionString)
    {
        this.connectionString = connectionString;
    }

    public IEnumerable<Product> GetFeaturedProducts()
    {
        using (var repository = this.Create())    ①  
        {
            return repository.GetFeaturedProducts();    ②  
        }
    }

    private SqlProductRepository Create()
    {
        return new SqlProductRepository(    ③  
            this.connectionString);
    }
}

请注意其私有方法在内部如何包含类似工厂的行为。然而,与从其定义中公开的抽象工厂相比,此行为被封装在代理中并且不会泄漏。SqlProductRepositoryProxyCreateIProductRepositoryFactoryIProductRepository

Notice how SqlProductRepositoryProxy internally contains factory-like behavior with its private Create method. This behavior, however, is encapsulated within the Proxy and doesn’t leak out, compared to the IProductRepositoryFactory Abstract Factory that exposes IProductRepository from its definition.

SqlProductRepositoryProxy与 紧密耦合SqlProductRepository。如果在您的域层中定义,这将是Control Freak反模式(第 5.1 节)的实现。相反,您应该在包含或更可能包含Composition Root的数据访问层中定义此代理。SqlProductRepositoryProxySqlProductRepository

SqlProductRepositoryProxy is tightly coupled to SqlProductRepository. This would be an implementation of the Control Freak anti-pattern (section 5.1) if the SqlProductRepositoryProxy was defined in your domain layer. Instead, you should either define this Proxy in your data access layer that contains SqlProductRepository or, more likely, the Composition Root.

因为该Create方法构成了对象图的一部分,所以Composition Root非常适合放置此 Proxy 类。下一个清单显示使用SqlProductRepositoryProxy.

Because the Create method composes part of the object graph, the Composition Root is a well-suited location to place this Proxy class. The next listing shows the structure of the Composition Root using the SqlProductRepositoryProxy.

清单 6.18 带有新的对象图SqlProductRepositoryProxy

Listing 6.18 Object graph with the new SqlProductRepositoryProxy

new HomeController(
    new SqlProductRepositoryProxy(    ①  
        connectionString));

在一个Abstraction有很多成员的情况下,创建 Proxy 实现变得相当麻烦。然而,具有许多成员的抽象通常违反接口隔离原则。使抽象更加集中可以解决许多问题,例如创建代理、装饰器和测试替身的复杂性。我们将在 6.3 节中更详细地讨论这个问题,并在第 10 章再次回到这个主题。

In the case that an Abstraction has many members, it becomes quite cumbersome to create Proxy implementations. Abstractions with many members, however, typically violate the Interface Segregation Principle. Making Abstractions more focused solves many problems, such as the complexity of creating Proxies, Decorators, and Test Doubles. We’ll discuss this in more detail in section 6.3 and again come back to this subject in chapter 10.

下一节将讨论滥用抽象工厂来根据提供的运行时数据选择要返回的依赖项。

The next section deals with the abuse of Abstract Factories to select the Dependency to return, based on the supplied runtime data.

6.2.2 滥用抽象工厂根据运行时数据选择依赖

6.2.2 Abusing Abstract Factories to select Dependencies based on runtime data

在上一节中,您了解到抽象工厂通常应该接受运行时值作为输入。没有它们,您将向消费者泄露有关实现的实现细节。这并不意味着接受运行时数据的抽象工厂是适用于所有情况的正确解决方案。通常情况下,事实并非如此。

In the previous section, you learned that Abstract Factories should typically accept runtime values as input. Without them, you’re leaking implementation details about the implementation to the consumer. This doesn’t mean that an Abstract Factory that accepts runtime data is the correct solution to every situation. More often than not, it isn’t.

在本节中,我们将查看抽象工厂,它们专门接受运行时数据来决定返回哪个依赖项。我们将查看的示例是提供计算两个位置之间路线的在线地图站点,我们在第 6.2 节开头介绍过。

In this section, we’ll look at Abstract Factories that accept runtime data specifically to decide which Dependency to return. The example we’ll look at is the online mapping site that offers to calculate a route between two locations, which we introduced at the start of section 6.2.

要计算路由,应用程序需要一种路由算法,但它并不关心是哪一种。每个选项代表一个不同的算法,应用程序可以将每个路由算法作为一个抽象来处理,以平等对待它们。您必须告诉应用程序要使用哪种算法,但直到运行时您才会知道这一点,因为它基于用户的选择。

To calculate a route, the application needs a routing algorithm, but it doesn’t care which one. Each option represents a different algorithm, and the application can handle each routing algorithm as an Abstraction to treat them all equally. You must tell the application which algorithm to use, but you won’t know this until runtime because it’s based on the user’s choice.

在 Web 应用程序中,您只能将基本类型从浏览器传输到服务器。当用户从下拉框中选择路由算法时,您必须用数字或字符串来表示。15   An是一个数字,所以在服务器上你可以使用这个来表示选择:enumRouteType

In a web application, you can only transfer primitive types from the browser to the server. When the user selects a routing algorithm from a drop-down box, you must represent this by a number or a string.15  An enum is a number, so on the server you can represent the selection using this RouteType:

public enum RouteType { Shortest, Fastest, Scenic }

您需要的是一个可以为您计算路线的实例:IRouteAlgorithm

What you need is an instance of IRouteAlgorithm that can calculate the route for you:

public interface IRouteAlgorithm
{
    RouteResult CalculateRoute(RouteSpecification specification);
}

现在你遇到了一个问题。这是基于用户选择的运行时数据。它与请求一起发送到服务器。RouteType

Now you’re presented with a problem. The RouteType is runtime data based on the user’s choice. It’s sent to the server with the request.

清单 6.19 及其方法RouteControllerGetRoute

Listing 6.19 RouteController with its GetRoute method

public class RouteController : Controller
{
    public ViewResult GetRoute(
        RouteSpecification spec, RouteType routeType)
    {
        IRouteAlgorithm algorithm = ...    ①  

        var route = algorithm.CalculateRoute(spec);    ②  

        var vm = new RouteViewModel    ③  
        {    ③  
            ...    ③  
        };    ③  

        return this.View(vm);    ④  
    }
}

现在的问题就变成了,如何得到合适的算法呢?如果您没有阅读本章,您对这一挑战的下意识反应可能是引入一个抽象工厂,如下所示:

The question now becomes, how do you get the appropriate algorithm? If you hadn’t been reading this chapter, your knee-jerk reaction to this challenge would probably be to introduce an Abstract Factory, like this:

public interface IRouteAlgorithmFactory
{
    IRouteAlgorithm CreateAlgorithm(RouteType routeType);
}

这使您能够通过注入和使用它将运行时值转换为您需要的依赖项来实现一种GetRoute方法。以下清单演示了交互。RouteControllerIRouteAlgorithmFactoryIRouteAlgorithm

This enables you to implement a GetRoute method for RouteController by injecting IRouteAlgorithmFactory and using it to translate the runtime value to the IRouteAlgorithm Dependency you need. The following listing demonstrates the interaction.

气味.tif

清单 6.20 使用inIRouteAlgorithmFactoryRouteController

Listing 6.20 Using an IRouteAlgorithmFactory in RouteController

public class RouteController : Controller
{
    private readonly IRouteAlgorithmFactory factory;

    public RouteController(IRouteAlgorithmFactory factory)
    {
        this.factory = factory;
    }

    public ViewResult GetRoute(
        RouteSpecification spec, RouteType routeType)
    {
        IRouteAlgorithm algorithm =    ①  
            this.factory.CreateAlgorithm(routeType);    ①  

        var route = algorithm.CalculateRoute(spec);    ②  

        var vm = new RouteViewModel
        {
            ...
        };

        return this.View(vm);
    }
}

该类RouteController的职责是处理网络请求。该GetRoute方法接收用户指定的起点和终点,以及一个选定的RouteType. 使用抽象工厂,您将运行RouteType时值映射到一个IRouteAlgorithm实例,因此您请求一个使用构造函数注入的实例。图 6.8显示了这种交互及其依赖关系的顺序。IRouteAlgorithmFactoryRouteController

The RouteController class’s responsibility is to handle web requests. The GetRoute method receives the user’s specification of origin and destination, as well as a selected RouteType. With an Abstract Factory, you map the runtime RouteType value to an IRouteAlgorithm instance, so you request an instance of IRouteAlgorithmFactory using Constructor Injection. This sequence of interactions between RouteController and its Dependencies is shown in figure 6.8.

最简单的实现将涉及一个 switch 语句并根据输入返回三个不同的实现。但我们会将其作为练习留给读者。IRouteAlgorithmFactoryIRouteAlgorithm

The most simple implementation of IRouteAlgorithmFactory would involve a switch statement and return three different implementations of IRouteAlgorithm based on the input. But we’ll leave this as an exercise for the reader.

到目前为止,您可能想知道,“有什么问题?为什么这是代码味道?” 为了能够看到问题,我们需要回到依赖倒置原则

Up until this point you might be wondering, “What’s the catch? Why is this a code smell?” To be able to see the problem, we need to go back to the Dependency Inversion Principle.

06-08.eps

图 6.8 RouteControllerrouteType运行时值提供给IRouteAlgorithmFactory。工厂返回一个IRouteAlgorithm实现,并RouteController通过调用请求路由CalculateRoute。交互类似于图 6.6中的交互。

Figure 6.8 RouteController supplies the routeType runtime value to IRouteAlgorithmFactory. The factory returns an IRouteAlgorithm implementation, and RouteController requests a route by calling CalculateRoute. The interaction is similar to that of figure 6.6.

代码气味分析

Analysis of the code smell

在第 3 章(3.1.2 节)中,我们讨论了依赖倒置原则。我们讨论了它如何声明抽象应该由使用抽象的层拥有。我们解释说,抽象的消费者应该决定其形状并以最适合其需求的方式定义抽象。当我们回过头来RouteController问自己这是否是RouteController最适合的设计时,我们会争辩说这种设计不适合RouteController

In chapter 3 (section 3.1.2), we talked about the Dependency Inversion Principle. We discussed how it states that Abstractions should be owned by the layer using the Abstraction. We explained that it’s the consumer of the Abstraction that should dictate its shape and define the Abstraction in a way that suits its needs the most. When we go back to our RouteController and ask ourselves whether this is the design that suits RouteController the best, we’d argue that this design doesn’t suit RouteController.

一种看待这个问题的方法是评估依赖 RouteController项的数量,这可以告诉您一些有关类复杂性的信息。正如您在 6.1 节中看到的,拥有大量依赖项是一种代码味道,典型的解决方案是应用 Facade Services 重构。

One way of looking at this is by evaluating the number of Dependencies RouteController has, which tells you something about the complexity of the class. As you saw in section 6.1, having a large number of Dependencies is a code smell, and a typical solution is to apply Facade Services refactoring.

当您引入抽象工厂时,您总是会增加消费者拥有的依赖项的数量。如果只看 的构造函数RouteController,您可能会认为控制器只有一个Dependency。但IRouteAlgorithm也是 的依赖RouteController,即使它没有注入到它的构造函数中也是如此。

When you introduce an Abstract Factory, you always increase the number of Dependencies a consumer has. If you only look at the constructor of RouteController, you may be led to believe that the controller only has one Dependency. But IRouteAlgorithm is also a Dependency of RouteController, even if it isn’t injected into its constructor.

这种增加的复杂性一开始可能并不明显,但是当您开始单元测试时可以立即感受到RouteController。这不仅迫使您测试与 with 的交互RouteControllerIRouteAlgorithm您还必须测试与 的交互IRouteAlgorithmFactory

This increased complexity might not be obvious at first, but it can be felt instantly when you start unit testing RouteController. Not only does this force you to test the interaction RouteController has with IRouteAlgorithm, you also have to test the interaction with IRouteAlgorithmFactory.

重构以获得更好的解决方案

Refactoring toward a better solution

您可以通过将两者合并在一起来减少依赖项的数量,就像您在 6.1 节的 Facade Services 重构中看到的那样。理想情况下,您希望以与在 6.2.1 节中应用的方式相同的方式使用代理模式。但是,代理仅在抽象提供了选择适当的依赖项所需的所有数据的情况下才适用。不幸的是,这个先决条件不成立,因为它只提供了一个,而不是一个.IRouteAlgorithmFactoryIRouteAlgorithmIRouteAlgorithmRouteSpecificationRouteType

You can reduce the number of Dependencies by merging both IRouteAlgorithmFactory and IRouteAlgorithm together, much like you saw with the Facade Services refactoring of section 6.1. Ideally, you’d want to use the Proxy pattern the same way you applied it in section 6.2.1. A Proxy, however, is only applicable in case the Abstraction is supplied with all the data required to select the appropriate Dependency. Unfortunately, this prerequisite doesn’t hold for IRouteAlgorithm because it’s only supplied with a RouteSpecification, but not a RouteType.

在放弃代理模式之前,重要的是要验证从概念层面传递RouteTypeIRouteAlgorithm. 如果是,则意味着实现包含选择正确算法和算法计算路由所需的运行时值所需的所有信息。然而,在这种情况下,传递给在概念上很奇怪。算法实现永远不需要使用. 相反,为了降低控制器的复杂性,您定义了一个适配器,它在内部调度到适当的路由算法:CalculateRouteRouteTypeIRouteAlgorithmRouteType

Before you discard the Proxy pattern, it’s important to verify whether it makes sense from a conceptual level to pass RouteType on to IRouteAlgorithm. If it does, it means that a CalculateRoute implementation contains all the information required to select both the proper algorithm and the runtime values the algorithm will need to calculate the route. In this case, however, passing RouteType on to IRouteAlgorithm is conceptually weird. An algorithm implementation will never need to use RouteType. Instead, to reduce the controller’s complexity, you define an Adapter that internally dispatches to the appropriate route algorithm:

public interface IRouteCalculator
{
    RouteResult Calculate(RouteSpecification spec, RouteType routeType);
}

以下清单显示了RouteController当依赖IRouteCalculator而不是IRouteAlgorithmFactory.

The following listing shows how RouteController gets simplified when it depends on IRouteCalculator instead of IRouteAlgorithmFactory.

好的.tif

清单 6.21 使用inIRouteCalculatorRouteController

Listing 6.21 Using an IRouteCalculator in RouteController

public class RouteController : Controller
{
    private readonly IRouteCalculator calculator;

    public RouteController(IRouteCalculator calculator)  ①  
    {
        this.calculator = calculator;
    }

    public ViewResult GetRoute(RouteSpecification spec, RouteType routeType)
    {
        var route = this.calculator.Calculate(spec, routeType);

        var vm = new RouteViewModel { ... };

        return this.View(vm);
    }
}

图 6.9RouteController显示了与其唯一依赖项之间的简化交互。正如您在图 6.7中看到的那样,交互被简化为单个方法调用。

Figure 6.9 shows the simplified interaction between RouteController and its sole Dependency. As you saw in figure 6.7, the interaction is reduced to a single method call.

06-09.eps

图 6.9图 6.8相比,通过隐藏IRouteAlgorithmFactoryIRouteAlgorithm隐藏在单个IRouteCalculatorAbstraction之后,简化RouteController了与其(现在是单个)Dependency之间的交互。

Figure 6.9 Compared to figure 6.8, by hiding IRouteAlgorithmFactory and IRouteAlgorithm behind a single IRouteCalculatorAbstraction, the interaction between RouteController and its (now single) Dependency is simplified.

您可以通过IRouteCalculator多种方式实施 。一种方法是注入this 。不过,这不是我们的偏好,因为这是一个无用的额外间接层,您可以轻松地摆脱它。相反,您会将实现注入到构造函数中。IRouteAlgorithmFactoryRouteCalculatorIRouteAlgorithmFactoryIRouteAlgorithmRouteCalculator

You can implement an IRouteCalculator in many ways. One way is to inject IRouteAlgorithmFactory into this RouteCalculator. This isn’t our preference, though, because IRouteAlgorithmFactory would be a useless extra layer of indirection you could easily do without. Instead, you’ll inject IRouteAlgorithm implementations into the RouteCalculator constructor.

清单 6.22 包装一个sIRouteCalculator的字典IRouteAlgorithm

Listing 6.22 IRouteCalculator wrapping a dictionary of IRouteAlgorithms

public class RouteCalculator : IRouteCalculator
{
    private readonly IDictionary<RouteType, IRouteAlgorithm> algorithms;

    public RouteCalculator(
        IDictionary<RouteType, IRouteAlgorithm> algorithms)
    {
        this.algorithms = algorithms;
    }

    public RouteResult Calculate(RouteSpecification spec, RouteType type)
    {
        return this.algorithms[type].CalculateRoute(spec);
    }
}

使用新定义的RouteCalculator,RouteController现在可以这样构造:

Using the newly defined RouteCalculator, RouteController can now be constructed like this:

var algorithms = new Dictionary<RouteType, IRouteAlgorithm>
{
    { RouteType.Shortest, new ShortestRouteAlgorithm() },
    { RouteType.Fastest, new FastestRouteAlgorithm() },
    { RouteType.Scenic, new ScenicRouteAlgorithm() }
};

new RouteController(
    new RouteCalculator(algorithms));

通过从抽象工厂重构为适配器,您可以有效地减少组件之间的依赖关系数量。图 6.10显示了使用工厂的初始解决方案的依赖关系图,而图 6.11显示了重构后的对象图。

By refactoring from Abstract Factory to an Adapter, you effectively reduce the number of Dependencies between your components. Figure 6.10 shows the Dependency graph of the initial solution using the Factory, while figure 6.11 shows the object graph after refactoring.

06-10.eps

图 6.10 with 的初始依赖关系RouteControllerIRouteAlgorithmFactory

Figure 6.10 The initial Dependency graph for RouteController with IRouteAlgorithmFactory

06-11.eps

图 6.11依赖代替RouteController时的依赖关系图IRouteCalculator

Figure 6.11 The Dependency graph for RouteController when depending on IRouteCalculator instead

当您使用抽象工厂根据提供的运行时数据选择依赖项时,通常情况下,您可以通过重构不像抽象工厂那样公开底层依赖项的适配器来降低复杂性。然而,这不仅仅适用于抽象工厂。我们想概括这一点。

When you use Abstract Factories to select Dependencies based on supplied runtime data, more often than not, you can reduce complexity by refactoring toward Adapters that don’t expose the underlying Dependency like the Abstract Factory does. This, however, doesn’t hold only when dealing with Abstract Factories. We’d like to generalize this point.

通常,服务抽象不应在其定义中公开其他服务抽象。16  这意味着服务抽象不应该接受另一个服务抽象作为输入,也不应该将服务抽象作为输出参数或返回类型。依赖于其他应用程序服务的应用程序服务迫使它们的客户了解这两种依赖关系

Typically, service Abstractions shouldn’t expose other service Abstractions in their definition.16  This means that a service Abstraction shouldn’t accept another service Abstraction as input, nor should it have service Abstractions as output parameters or as a return type. Application services that depend on other application services force their clients to know about both Dependencies.

下一个代码味道更奇特,所以您可能不会经常遇到它。尽管前面讨论的代码味道可能会被忽视,但下一个味道很难错过——您的代码要么停止编译,要么在运行时中断。

The next code smell is a more exotic one, so you might not encounter it that often. Although the previously discussed code smells can go unnoticed, the next smell is hard to miss — your code either stops compiling or breaks at runtime.

6.3 修复循环依赖

6.3 Fixing cyclic Dependencies

有时,依赖实现会变成循环的。一个实现需要另一个Dependency,它的实现需要第一个Abstraction。这样的依赖图是满足不了的。图 6.12显示了这个问题。

Occasionally, Dependency implementations turn out to be cyclic. An implementation requires another Dependency whose implementation requires the first Abstraction. Such a Dependency graph can’t be satisfied. Figure 6.12 shows this problem.

06-12.eps

图 6.12 和之间的依赖循环ChickenEgg

Figure 6.12 Dependency cycle between Chicken and Egg

下面显示了一个简单的示例,其中包含图6.12的循环依赖

The following shows a simplistic example containing the cyclic Dependency of figure 6.12:

public class Chicken : IChicken
{
    public Chicken(IEgg egg) { ... }    ①  

    public void HatchEgg() { ... }
}

public class Egg : IEgg    ②  
{
    public Egg(IChicken chicken) { ... }    ③  
}

考虑到前面的示例,您如何构建由这些类组成的对象图?

With the previous example in mind, how can you construct an object graph consisting of these classes?

new Chicken(    ①  
    new Egg(
        ???    ②  
    )
);

我们在这里得到的是典型的先有鸡还是先有蛋的因果关系困境。简短的回答是你不能像这样构造一个对象图,因为这两个类都需要另一个对象在它们被构造之前存在。只要循环存在,您就不可能满足所有Dependencies,您的应用程序将无法运行。显然,必须做点什么,但是做什么呢?

What we’ve got here is your typical the chicken or the egg causality dilemma. The short answer is that you can’t construct an object graph like this because both classes require the other object to exist before they’re constructed. As long as the cycle remains, you can’t possibly satisfy all Dependencies, and your applications won’t be able to run. Clearly, something must be done, but what?

在本节中,我们将研究有关循环依赖的问题,包括一个示例。当我们完成后,您的第一反应应该是尝试重新设计您的Dependencies,因为问题通常是由您的应用程序设计引起的。因此,本节的主要内容是:依赖循环通常是由违反 SRP 引起的。

In this section, we’ll look into the issue concerning cyclic Dependencies, including an example. When we’re finished, your first reaction should be to try to redesign your Dependencies, because the problem is typically caused by your application’s design. The main takeaway from this section, therefore, is this: Dependency cycles are typically caused by an SRP violation.

如果无法重新设计依赖项,则可以通过从构造函数注入重构为属性注入来打破循环。这代表了类的不变量的松动,所以这不是你应该轻易做的事情。

If redesigning your Dependencies isn’t possible, you can break the cycle by refactoring from Constructor Injection to Property Injection. This represents a loosening of a class’s invariants, so it isn’t something you should do lightly.

6.3.1 示例:SRP 违规导致的依赖循环

6.3.1 Example: Dependency cycle caused by an SRP violation

Mary Rowan(第 2 章的开发人员)开发她的电子商务应用程序已经有一段时间了,并且在生产中非常成功。然而,有一天,玛丽的老板突然上门要求一项新功能。抱怨是当生产中出现问题时,很难确定谁在处理系统中的特定数据。一种解决方案是将更改存储在审计表中,该表记录系统中每个用户所做的每项更改。

Mary Rowan (our developer from chapter 2) has been developing her e-commerce application for some time now, and it’s been quite successful in production. One day, however, Mary’s boss pops around the door to request a new feature. The complaint is that when problems arise in production, it’s hard to pinpoint who’s been working on a certain piece of data in the system. One solution would be to store changes in an auditing table that records every change that every user in the system makes.

考虑了一段时间后,Mary 提出了Abstraction的定义,如代码清单 6.23所示。(请注意,为了在现实环境中展示这种代码味道,我们需要一个稍微复杂的示例。下面的示例包含三个类,在我们开始分析之前,我们将花几页来解释代码。)IAuditTrailAppender

After thinking about this for some time, Mary comes up with the definition for an IAuditTrailAppender Abstraction, as shown in listing 6.23. (Note that to demonstrate this code smell in a realistic setting, we need a somewhat complex example. The following example consists of three classes, and we’ll spend a few pages explaining the code, before we get to its analysis.)

清单 6.23 一个抽象IAuditTrailAppender

Listing 6.23 An IAuditTrailAppenderAbstraction

public interface IAuditTrailAppender    ①  
{
    void Append(Entity changedEntity);    ②  
}

Mary 使用 SQL Server Management Studio 创建一个 AuditEntries 表,她可以用它来存储审计条目。表定义如表 6.2所示。

Mary uses SQL Server Management Studio to create an AuditEntries table that she can use to store the audit entries. The table definition is shown in table 6.2.

表 6.2 Mary 的 AuditEntries 表
列名数据类型允许空值首要的关键
ID唯一标识符是的
用户身份唯一标识符
变化时间约会时间
实体ID唯一标识符
实体类型变量(100)

创建数据库表后,Mary 继续IAuditTrailAppender实施,如下一个清单所示。

After creating her database table, Mary continues with the IAuditTrailAppender implementation, shown in the next listing.

清单 6.24 将条目附加到 SQL 数据库表SqlAuditTrailAppender

Listing 6.24 SqlAuditTrailAppender appends entries to a SQL database table

public class SqlAuditTrailAppender : IAuditTrailAppender
{
    private readonly IUserContext userContext;
    private readonly CommerceContext context;
    private readonly ITimeProvider timeProvider;    ①  

    public SqlAuditTrailAppender(
        IUserContext userContext,
        CommerceContext context,
        ITimeProvider timeProvider)
    {
        this.userContext = userContext;
        this.context = context;
        this.timeProvider = timeProvider;
    }

    public void Append(Entity changedEntity)
    {
        AuditEntry entry = new AuditEntry
        {
            UserId = this.userContext.CurrentUser.Id,    ②  
            TimeOfChange = this.timeProvider.Now,    ②  
            EntityId = entity.Id,    ②  
            EntityType = entity.GetType().Name    ②  
        };

        this.context.AuditEntries.Add(entry);
    }
}

审计跟踪的一个重要部分是将更改与用户相关联。为此,SqlAuditTrailAppender需要一个IUserContext Dependency。这允许使用 上的属性构造条目。这是 Mary 前段时间为另一个功能添加的属性。SqlAuditTrailAppenderCurrentUserIUserContext

An important part of an audit trail is relating a change to a user. To accomplish this, SqlAuditTrailAppender requires an IUserContext Dependency. This allows SqlAuditTrailAppender to construct the entry using the CurrentUser property on IUserContext. This is a property that Mary added some time ago for another feature.

清单 6.25显示了 Mary 的当前版本(初始版本见清单 3.12)。AspNetUserContextAdapter

Listing 6.25 shows Mary’s current version of the AspNetUserContextAdapter (see listing 3.12 for the initial version).

添加属性的清单 6.25AspNetUserContextAdapterCurrentUser

Listing 6.25 AspNetUserContextAdapter with added CurrentUser property

public class AspNetUserContextAdapter : IUserContext
{
    private static HttpContextAccessor Accessor = new HttpContextAccessor();

    private readonly IUserRepository repository;    ①  

    public AspNetUserContextAdapter(
        IUserRepository repository)
    {
        this.repository = repository;
    }

    public User CurrentUser    ②  
    {
        get
        {
            var user = Accessor.HttpContext.User;    ③  
            string userName = user.Identity.Name;    ③  
            return this.repository.GetByName(userName);  ③  
        }
    }
    ...
}

当您忙于阅读有关 DI 模式和反模式的内容时,Mary 也很忙。IUserRepository是她同时添加的抽象之一。我们将很快讨论她的IUserRepository实现。

While you were busy reading about DI patterns and anti-patterns, Mary’s been busy too. IUserRepository is one of the Abstractions she added in the meantime. We’ll discuss her IUserRepository implementation shortly.

Mary 的下一步是更新需要附加到审计跟踪的类。需要更新的类之一是SqlUserRepository. 它实现IUserRepository了 ,所以现在是看一看它的好时机。以下清单显示了此类的相关部分。

Mary’s next step is to update the classes that need to be appended to the audit trail. One of the classes that needs to be updated is SqlUserRepository. It implements IUserRepository, so this is a good moment to take a peek at it. The following listing shows the relevant parts of this class.

清单 6.26 需要附加到审计跟踪SqlUserRepository

Listing 6.26 SqlUserRepository that needs to append to the audit trail

public class SqlUserRepository : IUserRepository
{
    public SqlUserRepository(
        CommerceContext context,
        IAuditTrailAppender appender)    ①  
    {
        this.appender = appender;
        this.context = context;
    }

    public void Update(User user)
    {
        this.appender.Append(user);    ②  
        ...    ③  
    }

    public User GetById(Guid id) { ... }    ③  

    public User GetByName(string name) { ... }    ④  
}

玛丽几乎完成了她的功能。因为她向该SqlUserRepository方法添加了一个构造函数参数,所以她只剩下更新Composition Root了。目前,创建的Composition Root部分如下所示:AspNetUserContextAdapter

Mary is almost finished with her feature. Because she added a constructor argument to the SqlUserRepository method, she’s left with updating the Composition Root. Currently, the part of the Composition Root that creates AspNetUserContextAdapter looks like this:

var userRepository = new SqlUserRepository(context);

IUserContext userContext = new AspNetUserContextAdapter(userRepository);

因为被添加为构造函数的依赖项,Mary 尝试将其添加到Composition RootIAuditTrailAppenderSqlUserRepository

Because IAuditTrailAppender was added as Dependency to the SqlUserRepository constructor, Mary tries to add it to the Composition Root:

var appender = new SqlAuditTrailAppender(
    userContext,    ①  
    context,
    timeProvider);

var userRepository = new SqlUserRepository(context, appender);

IUserContext userContext = new AspNetUserContextAdapter(userRepository);

不幸的是,Mary 的更改无法编译。C# 编译器会抱怨:“无法在局部变量‘userContext’声明之前使用它。”

Unfortunately, Mary’s changes don’t compile. The C# compiler complains: “Cannot use local variable 'userContext' before it’s declared.”

由于依赖于,Mary 尝试为 提供她定义的变量。C# 编译器不接受这一点,因为这样的变量必须在使用前定义。Mary 试图通过移动变量的定义和赋值来解决问题SqlAuditTrailAppenderIUserContextSqlAuditTrailAppenderuserContextuserContext向上,但这会立即导致 C# 编译器抱怨该userRepository变量。但是当她移动userRepository变量时向上,编译器抱怨这个appender变量,它在声明之前就被使用了。

Because SqlAuditTrailAppender depends on IUserContext, Mary tries to supply the SqlAuditTrailAppender with the userContext variable that she defined. The C# compiler doesn’t accept this because such a variable must be defined before it’s used. Mary tries to fix the problem by moving the definition and assignment of the userContext variable up, but this immediately causes the C# compiler to complain about the userRepository variable. But when she moves the userRepository variable up, the compiler complaints about the appender variable, which is used before it’s declared.

玛丽开始意识到她遇到了严重的麻烦——她的依赖关系图中有一个循环。让我们分析一下哪里出了问题。

Mary starts to realize she’s in serious trouble — there’s a cycle in her Dependency graph. Let’s analyze what went wrong.

6.3.2 玛丽的依赖循环分析

6.3.2 Analysis of Mary’s Dependency cycle

一旦她将IAuditTrailAppender 依赖项添加到SqlUserRepository类中,玛丽的对象图中的循环就会出现. 图 6.13显示了这个依赖循环。

The cycle in Mary’s object graph appeared once she added the IAuditTrailAppender Dependency to the SqlUserRepository class. Figure 6.13 shows this Dependency cycle.

06-13.eps

图 6.13涉及、和 的依赖循环AspNetUserContextAdapterSqlUserRepositorySqlAuditTrailAppender

Figure 6.13 The Dependency cycle involving AspNetUserContextAdapter, SqlUserRepository, and SqlAuditTrailAppender

该图显示了对象图中的循环。然而,对象图是故事的一部分。我们可以用来分析问题的另一个视图是方法调用图,如下所示:

The figure shows the cycle in the object graph. The object graph, however, is part of the story. Another view we can use to analyze the problem is the method call graph as shown here:

UserService.UpdateMailAddress(Guid userId, string newMailAddress)
    ➥ SqlUserRepository.Update(User user)
        ➥ SqlAuditTrailAppender.Append(Entity changedEntity)
            ➥ AspNetUserContextAdapter.CurrentUser
                ➥ SqlUserRepository.GetByName(string name)

此调用图显示调用将如何从 的UpdateMailAddress方法开始UserService,这将调用类的Update方法SqlUserRepository. 从那里它进入SqlAuditTrailAppender,然后进入AspNetUserContextAdapter,最后,它在SqlUserRepositoryGetByName方法中结束.

This call graph shows how the call would start with the UpdateMailAddress method of UserService, which would call into the Update method of the SqlUserRepository class. From there it goes into SqlAuditTrailAppender, then into AspNetUserContextAdapter and, finally, it ends up in the SqlUserRepository’s GetByName method.

这个方法调用图表明,虽然对象图是循环的,但方法调用图不是递归的。GetByName例如,如果再次调用,它将变为递归SqlAuditTrailAppender.Append。这将导致无休止地调用其他方法,直到进程用完堆栈空间,从而导致. 对 Mary 来说幸运的是,调用图不是递归的,因为这需要她重写方法。问题的原因在别处——违反了 SRP。StackOverflowException

What this method call graph shows is that although the object graph is cyclic, the method call graph isn’t recursive. It would become recursive if GetByName again called SqlAuditTrailAppender.Append, for instance. That would cause the endless calling of other methods until the process ran out of stack space, causing a StackOverflowException. Fortunately for Mary, the call graph isn’t recursive, as that would require her to rewrite the methods. The cause of the problem lies somewhere else — there’s an SRP violation.

当我们查看之前声明的类AspNetUserContextAdapterSqlUserRepositorySqlAuditTrailAppender时,您可能会发现很难发现可能的 SRP 违规。如表 6.3 所列,所有三个班级似乎都专注于一个特定领域。

When we take a look at the previously declared classes AspNetUserContextAdapter, SqlUserRepository, and SqlAuditTrailAppender, you might find it difficult to spot a possible SRP violation. All three classes seem to be focused on one particular area, as table 6.3 lists.

表 6.3抽象及其在应用程序中的 作用
抽象角色方法
IAuditTrailAppender启用记录用户所做的重要更改1个方法
IUserContext向消费者提供有关代表其执行当前请求的用户的信息2种方法
IUserRepository为给定的持久性技术提供围绕用户的检索、查询和存储的操作3种方法

如果您更仔细地查看IUserRepository,您会发现类中的功能主要围绕用户的概念进行分组。这是一个相当宽泛的概念。如果您坚持这种将与用户相关的方法分组到一个类中的方法,您会看到这两种方法IUserRepository并且SqlUserRepository经常被更改。

If you look more closely at IUserRepository, you can see that the functionality in the class is primarily grouped around the concept of a user. This is a quite broad concept. If you stick with this approach of grouping user-related methods in a single class, you’ll see both IUserRepository and SqlUserRepository being changed quite frequently.

当我们从内聚的角度来看 SRP 时,我们可以问问自己,里面的方法IUserRepository是否真的有那么高的内聚性。将类拆分为多个更窄的接口和类有多容易?

When we look at the SRP from the perspective of cohesion, we can ask ourselves whether the methods in IUserRepository are really that highly cohesive. How easy would it be to split the class up into multiple narrower interfaces and classes?

6.3.3 重构 SRP 违规以解决依赖循环

6.3.3 Refactoring from SRP violations to resolve the Dependency cycle

修复 SRP 违规可能并不总是那么容易,因为这可能会导致Abstraction的消费者发生连锁反应。然而,对于我们的小型商务应用程序,进行更改非常容易,如以下清单所示。

It might not always be easy to fix SRP violations, because that might cause rippling changes through the consumers of the Abstraction. In the case of our little commerce application, however, it’s quite easy to make the change, as the following listing shows.

好的.tif

清单 6.27 移入GetByNameIUserByNameRetriever

Listing 6.27 GetByName moved into IUserByNameRetriever

public interface IUserByNameRetriever
{
    User GetByName(string name);    ①  
}

public class SqlUserByNameRetriever : IUserByNameRetriever
{
    public SqlUserByNameRetriever(CommerceContext context)
    {
        this.context = context;
    }

    public User GetByName(string name) { ... }
}

在清单中,该GetByName方法从一个名为and的新抽象实现对中提取出来,IUserRepository并提取SqlUserRepository到一个新的抽象实现对中。新的实现不依赖于. 的剩余部分如下所示。IUserByNameRetrieverSqlUserByNameRetrieverSqlUserByNameRetrieverIAuditTrailAppenderSqlUserRepository

In the listing, the GetByName method is extracted from IUserRepository and SqlUserRepository into a new Abstraction implementation pair named IUserByNameRetriever and SqlUserByNameRetriever. The new SqlUserByNameRetriever implementation doesn’t depend on IAuditTrailAppender. The remaining part of SqlUserRepository is shown next.

好的.tif

清单 6.28 的剩余部分及其实现IUserRepository

Listing 6.28 The remaining part of IUserRepository and its implementation

public interface IUserRepository    ①  
{
    void Update(User user);
    User GetById(Guid id);
}

public class SqlUserRepository : IUserRepository
{
    public SqlUserRepository(    ②  
        CommerceContext context,
        IAuditTrailAppender appender
    {
        this.context = context;
        this.appender = appender;
    }

    public void Update(User user) { ... }
    public User GetById(Guid id) { ... }
}

玛丽从这个部门学到了一些东西。首先,新类更小,更容易理解。其次,它降低了玛丽不断更新现有代码的情况的可能性。最后但并非最不重要的一点是,拆分SqlUserRepository班级打破了依赖循环,因为新的SqlUserByNameRetriever不依赖于IAuditTrailAppender. 图 6.14显示了依赖循环是如何被打破的。

Mary gained a couple of things from this division. First of all, the new classes are smaller and easier to comprehend. Next, it lowers the chance of getting into the situation where Mary will be constantly updating existing code. And last, but not least, splitting the SqlUserRepository class breaks the Dependency cycle, because the new SqlUserByNameRetriever doesn’t depend on IAuditTrailAppender. Figure 6.14 shows how the Dependency cycle was broken.

06-14.eps

图 6.14 分离IUserRepository成两个接口打破了依赖循环。

Figure 6.14 The separation of IUserRepository into two interfaces breaks the Dependency cycle.

以下代码显示了将所有内容联系在一起的新Composition Root :

The following code shows the new Composition Root that ties everything together:

var userContext = new AspNetUserContextAdapter(
    new SqlUserByNameRetriever(context));    ①  

var appender = new
    SqlAuditTrailAppender(
        userContext,
        context,
        timeProvider);

var repository = new SqlUserRepository(context, appender);

依赖循环的最常见原因是违反 SRP。通过将类分解为更小、更集中的类来修复违规通常是一个很好的解决方案,但也有其他打破依赖循环的策略。

The most common cause of Dependency cycles is an SRP violation. Fixing the violation by breaking classes into smaller, more focused classes is typically a good solution, but there are also other strategies for breaking Dependency cycles.

6.3.4 打破依赖循环的常用策略

6.3.4 Common strategies for breaking Dependency cycles

当我们遇到依赖循环时,我们的第一个问题是,“我在哪里失败了?” 依赖循环应立即触发对根本原因的全面评估。任何循环都是一种设计味道,所以你的第一反应应该是重新设计涉及的部分,从一开始就防止循环发生。表 6.4显示了一些您可以采取的一般指导。

When we encounter a Dependency cycle, our first question is, “Where did I fail?” A Dependency cycle should immediately trigger a thorough evaluation of the root cause. Any cycle is a design smell, so your first reaction should be to redesign the involved part to prevent the cycle from happening in the first place. Table 6.4 shows some general directions you can take.

表 6.4 打破依赖循环的一些重新设计策略,从最优选到最不优选的策略排序
战略描述
分班正如您在审计跟踪示例中看到的那样,在大多数情况下,您可以将具有太多方法的类拆分为更小的类以打破循环。
.NET 事件您通常可以通过更改其中一个抽象来引发事件来打破循环,而不必显式调用依赖项来通知依赖项发生了某些事情。如果一侧仅在其Dependency上调用 void 方法,则事件特别合适。
属性注入如果一切都失败了,您可以通过将一个类从构造函数注入重构为属性注入来打破循环。这应该是最后的努力,因为它只能治疗症状。

别搞错了:依赖循环是一种设计味道。您的首要任务应该是分析代码以了解循环出现的原因。尽管如此,有时您无法更改设计,即使您了解循环的根本原因。

Make no mistake: a Dependency cycle is a design smell. Your first priority should be to analyze the code to understand why the cycle appears. Still, sometimes you can’t change the design, even if you understand the root cause of the cycle.

6.3.5 最后的手段:用属性注入打破循环

6.3.5 Last resort: Breaking the cycle with Property Injection

在某些情况下,设计错误是你无法控制的,但你仍然需要打破这个循环。在这种情况下,您可以使用Property Injection来做到这一点,即使它是一个临时解决方案。

In some cases, the design error is out of your control, but you still need to break the cycle. In such cases, you can do this by using Property Injection, even if it’s a temporary solution.

要打破循环,您必须对其进行分析以找出可以切入的位置。因为使用Property Injection建议一个可选的而不是必需的Dependency,所以您仔细检查所有Dependencies以确定切割伤害最小的地方是很重要的。

To break the cycle, you must analyze it to figure out where you can make a cut. Because using Property Injection suggests an optional rather than a required Dependency, it’s important that you closely inspect all Dependencies to determine where cutting hurts the least.

在我们的审计跟踪示例中,您可以通过将Constructor Injection的Dependency更改为Property Injection来解决循环。这意味着您可以先创建,将其注入,然后再分配给,如清单所示。SqlAuditTrailAppenderSqlAuditTrailAppenderSqlUserRepositoryAspNetUserContextAdapterSqlAuditTrailAppender

In our audit trail example, you can resolve the cycle by changing the Dependency of SqlAuditTrailAppender from Constructor Injection to Property Injection. This means that you can create SqlAuditTrailAppender first, inject it into SqlUserRepository, and then subsequently assign AspNetUserContextAdapter to SqlAuditTrailAppender, as this listing shows.

气味.tif

清单 6.29使用属性注入 打破依赖循环

Listing 6.29 Breaking a Dependency cycle with Property Injection

var appender =
    new SqlAuditTrailAppender(context, timeProvider);    ①  

var repository =
    new SqlUserRepository(context, appender);    ②  

var userContext = new 
AspNetUserContextAdapter(
    new SqlUserByNameRetriever(context));

appender.UserContext = userContext;    ③  

使用属性注入这种方式给 增加了额外的复杂性SqlAuditTrailAppender,因为它现在必须能够处理尚不可用的依赖项。这导致时间耦合,如第 4.3.2 节所述。

Using Property Injection this way adds extra complexity to SqlAuditTrailAppender, because it must now be able to deal with a Dependency that isn’t yet available. This leads to Temporal Coupling, as discussed in section 4.3.2.

如果您不想以这种方式放松任何原始类,一个密切相关的方法是引入一个虚拟代理,它保持SqlAuditTrailAppender不变:17 

If you don’t want to relax any of the original classes in this way, a closely related approach is to introduce a Virtual Proxy, which leaves SqlAuditTrailAppender intact:17 

气味.tif

清单 6.30使用虚拟代理 打破依赖循环

Listing 6.30 Breaking a Dependency cycle with a Virtual Proxy

var lazyAppender = new LazyAuditTrailAppender();    ①  

var repository =
    new SqlUserRepository(context, lazyAppender);

var userContext = new 
AspNetUserContextAdapter(
    new SqlUserByNameRetriever(context));

lazyAppender.Appender =    ②  
    new SqlAuditTrailAppender(
        userContext, context, timeProvider);

LazyAuditTrailAppender实现一样。但它通过Property Injection而不是Constructor Injection获取其依赖关系,从而允许您在不违反原始类的不变量的情况下打破循环。下一个清单显示了虚拟代理。IAuditTrailAppenderSqlAuditTrailAppenderIAuditTrailAppender LazyAuditTrailAppender

LazyAuditTrailAppender implements IAuditTrailAppender like SqlAuditTrailAppender does. But it takes its IAuditTrailAppender Dependency through Property Injection instead of Constructor Injection, allowing you to break the cycle without violating the invariants of the original classes. The next listing shows the LazyAuditTrailAppender Virtual Proxy.

清单 6.31 虚拟代理实现LazyAuditTrailAppender

Listing 6.31 A LazyAuditTrailAppender Virtual Proxy implementation

public class LazyAuditTrailAppender : IAuditTrailAppender
{
    public IAuditTrailAppender Appender { get; set; }    ①  

    public void Append(Entity changedEntity)
    {
        if (this.Appender == null)    ②  
        {
            throw new InvalidOperationException("Appender was not set.");
        }

        this.Appender.Append(changedEntity);    ③  
    }
}

请始终记住,解决循环的最佳方法是重新设计 API,使循环消失。但在极少数情况下,这是不可能的或非常不受欢迎的,您必须至少在一个地方使用属性注入来打破循环。这使您能够组合与属性关联的依赖关系之外的对象图的其余部分。当对象图的其余部分完全填充时,您可以通过属性注入适当的实例。属性注入表示依赖项是可选的,因此您不应该轻易进行更改。

Always keep in mind that the best way to address a cycle is to redesign the API so that the cycle disappears. But in the rare cases where this is impossible or highly undesirable, you must break the cycle by using Property Injection in at least one place. This enables you to compose the rest of the object graph apart from the Dependency associated with the property. When the rest of the object graph is fully populated, you can inject the appropriate instance via the property. Property Injection signals that a Dependency is optional, so you shouldn’t make the change lightly.

当您了解一些基本原理时,DI 并不是特别困难。然而,在您学习的过程中,您肯定会遇到可能让您困惑一段时间的问题。本章讨论了人们遇到的一些最常见的问题。它与前两章一起构成了模式、反模式和代码味道的目录。该目录构成本书的第 2 部分。在第 3 部分中,我们将转向 DI 的三个维度:对象组合生命周期管理拦截

DI isn’t particularly difficult when you understand a few basic principles. As you learn, however, you’re guaranteed to run into issues that may leave you stumped for a while. This chapter addressed some of the most common issues people encounter. Together with the two preceding chapters, it forms a catalog of patterns, anti-patterns, and code smells. This catalog constitutes part 2 of the book. In part 3, we’ll turn toward the three dimensions of DI: Object Composition, Lifetime Management, and Interception.

概括

Summary

  • 不断变化的抽象是违反单一职责原则(SRP)的明显标志。这也与开/闭原则有关,该原则规定您应该能够添加功能而无需更改现有类。
  • Ever-changing Abstractions are a clear sign of Single Responsibility Principle (SRP) violations. This also relates to the Open/Closed Principle that states that you should be able to add features without having to change existing classes.
  • 一个类拥有的方法越多,它违反 SRP 的可能性就越大。这也与Interface Segregation Principle相关,该原则规定不应强迫任何客户端依赖它不使用的方法。
  • The more methods a class has, the higher the chance it violates the SRP. This is also related to the Interface Segregation Principle, which states that no client should be forced to depend on methods it doesn’t use.
  • 简化抽象可以解决许多问题,例如创建代理、装饰器和测试替身的复杂性。
  • Making Abstractions thinner solves many problems, such as the complexity of creating Proxies, Decorators, and Test Doubles.
  • 构造函数注入的一个好处是,当您违反 SRP 时,它会变得更加明显。当一个类有太多Dependencies时,这是一个信号,你应该重新设计它。
  • A benefit of Constructor Injection is that it becomes more obvious when you violate the SRP. When a single class has too many Dependencies, it’s a signal that you should redesign it.
  • 当构造函数的参数列表变得太大时,我们将这种现象称为构造函数过度注入,并将其视为代码异味。这是一种与 DI 无关但被 DI 放大的一般代码味道。
  • When a constructor’s parameter list grows too large, we call the phenomenon Constructor Over-injection and consider it a code smell. It’s a general code smell unrelated to, but magnified by, DI.
  • 您可以通过多种方式从构造函数过度注入中进行重新设计,但是根据众所周知的设计模式将一个大类拆分为更小、更集中的类始终是一个不错的举措。
  • You can redesign from Constructor Over-injection in many ways, but splitting up a large class into smaller, more focused classes according to well-known design patterns is always a good move.
  • 您可以通过应用 Facade Services 重构来重构远离构造函数过度注入。Facade Service 隐藏了一个自然的交互依赖集群,它们的行为隐藏在一个Abstraction后面。
  • You can refactor away from Constructor Over-injection by applying Facade Services refactoring. A Facade Service hides a natural cluster of interacting Dependencies with their behavior behind a single Abstraction.
  • Facade Service 重构允许发现这些自然集群,并公开绘制以前未发现的关系和领域概念。Facade Service 与参数对象相关,但它不是组合和暴露组件,而是仅暴露封装的行为而隐藏成分。
  • Facade Service refactoring allows discovering these natural clusters and draws previously undiscovered relationships and domain concepts out in the open. Facade Service is related to Parameter Objects but, instead of combining and exposing components, it exposes only the encapsulated behavior while hiding the constituents.
  • 您可以通过将领域事件引入您的应用程序来重构远离构造函数过度注入。使用域事件,您可以捕获可以触发您正在开发的应用程序状态发生变化的操作。
  • You can refactor away from Constructor Over-injection by introducing domain events into your application. With domain events, you capture actions that can trigger a change to the state of the application you’re developing.
  • Leaky Abstraction是一种抽象例如接口,它会泄漏实现细节,例如特定于层的类型或特定于实现的行为。
  • A Leaky Abstraction is an Abstraction, such as an interface, that leaks implementation details, such as layer-specific types or implementation-specific behavior.
  • 实现的抽象IDisposableLeaky AbstractionsIDisposable应该在实施中生效。
  • Abstractions that implement IDisposable are Leaky Abstractions. IDisposable should be put into effect within the implementation instead.
  • 从概念上讲,服务抽象只有一个实例。将这些知识泄漏给消费者的抽象在设计时并没有考虑到这些消费者。
  • Conceptually, there’s only one instance of a service Abstraction. Abstractions that leak this knowledge to their consumers aren’t designed with those consumers in mind.
  • 服务抽象通常不应在其定义中公开其他服务抽象。依赖于其他抽象的抽象迫使他们的客户了解这两个抽象
  • Service Abstractions should typically not expose other service Abstractions in their definition. Abstractions that depend on other Abstractions force their clients to know about both Abstractions.
  • 在应用 DI 时,抽象工厂经常被过度使用。在许多情况下,存在更好的选择。
  • When it comes to applying DI, Abstract Factories are often overused. In many cases, better alternatives exist.
  • 抽象工厂创建的依赖项在概念上应该需要一个运行时值。从运行时值到抽象的转换在概念层面上应该是有意义的。如果您迫切希望引入一个抽象工厂来创建具体实现的实例,那么您可能手头有一个Leaky Abstraction 。相反,代理模式为您提供了更好的解决方案。
  • The Dependencies created by an Abstract Factory should conceptually require a runtime value. The translation from a runtime value into an Abstraction should make sense on the conceptual level. If you feel the urge to introduce an Abstract Factory to be able to create instances of a concrete implementation, you may have a Leaky Abstraction on hand. Instead, the Proxy pattern provides you with a better solution.
  • 在某些类中具有类似工厂的行为通常是不可避免的。然而,应用程序范围的工厂抽象应该被怀疑地审查。
  • Having factory-like behavior inside some classes is typically unavoidable. Application-wide Factory Abstractions, however, should be reviewed with suspicion.
  • 抽象工厂总是会增加消费者所拥有的依赖项的数量及其复杂性。
  • An Abstract Factory always increases the number of Dependencies a consumer has, along with its complexity.
  • 当您使用抽象工厂根据提供的运行时数据选择依赖项时,通常情况下,您可以通过重构不暴露底层依赖项的外观来降低复杂性。
  • When you use Abstract Factories to select Dependencies based on supplied runtime data, more often than not, you can reduce complexity by refactoring towards Facades that don’t expose the underlying Dependency.
  • 依赖循环通常是由违反 SRP 引起的。
  • Dependency cycles are typically caused by SRP violations.
  • 改进包含依赖循环的应用程序部分的设计应该是您的首选。在大多数情况下,这意味着将班级分成更小、更集中的班级。
  • Improving the design of the part of the application that contains the Dependency cycle should be your preferred option. In the majority of cases, this means splitting up classes into smaller, more focused classes.
  • 可以使用Property Injection打破依赖循环。您应该只通过使用属性注入来解决循环问题,作为最后的努力。它只是治标不治本。
  • Dependency cycles can be broken using Property Injection. You should only resort to solving cycles by using Property Injection as a last-ditch effort. It only treats the symptoms instead of curing the illness.
  • 类永远不应在其构造函数中执行涉及依赖项的工作,因为注入的依赖项可能尚未完全初始化。
  • Classes should never perform work involving Dependencies in their constructors because the injected Dependency may not yet be fully initialized.

第 3 部分

纯 DI

Part 3

Pure DI

在第 1 章中,我们简要概述了 DI 的三个维度:对象组合生命周期管理拦截。在本书的这一部分,我们将深入探讨这些维度,并为每个维度提供自己的章节。许多DI 容器具有与这些维度直接相关的特性。一些提供所有三个维度的功能,而另一些仅支持其中的一部分。

In chapter 1, we gave a short outline of the three dimensions of DI: Object Composition, Lifetime Management, and Interception. In this part of the book, we’ll explore these dimensions in depth, providing each with their own chapter. Many DI Containers have features that directly relate to these dimensions. Some provide features in all three dimensions, whereas others only support some of them.

由于DI 容器是一个可选工具,我们认为解释容器通常用于实现这些功能的基本原理和技术更为重要。鉴于此,第 3 部分探讨了如何在完全不使用DI 容器的情况下应用 DI。一个实用的自己动手的指南,这就是我们所说的纯 DI

Because a DI Container is an optional tool, we feel it’s more important to explain the underlying principles and techniques that containers typically use to implement these features. Given this, part 3 examines how to apply DI without using a DI Container at all. A practical do-it-yourself guide, this is what we call Pure DI.

第 7 章解释了如何在各种框架(如 ASP.NET Core MVC、控制台应用程序等)中组合对象。并非所有框架都同样好地支持 DI,即使在支持的框架中,细节也有很大差异。对于每个框架,可能很难识别启用 DI 的Seam。但是,一旦找到该Seam,您就有了适用于所有使用该特定框架的应用程序的解决方案。在第 7 章中,我们为最常见的 .NET 应用程序框架完成了这项工作。将其视为框架Seams的目录。

Chapter 7 explains how to compose objects in various frameworks like ASP.NET Core MVC, Console Applications, and so on. Not all frameworks support DI equally well, and even among those that do, the details differ a lot. For each framework, it can be difficult to identify the Seam that enables DI. Once that Seam is found, however, you have a solution for all applications that use that particular framework. In chapter 7, we’ve done this work for the most common .NET application frameworks. Think of it as a catalog of framework Seams.

尽管使用Pure DI组合对象并不是特别困难,但在阅读第 8 章中的生命周期管理后,您应该开始看到真正的DI 容器的好处。可以正确管理对象图中各种对象的生命周期,但它需要比Object Composition更多的自定义代码。而且这些代码都没有为应用程序增加任何特定的商业价值。除了解释终身管理的基础知识外,第 8 章还包含常见生活方式的目录。该目录用作第 4 部分中讨论生活方式的词汇表。尽管您不必手动实现其中的任何一个,但最好了解它们的工作原理。

Although composing objects isn’t particularly hard with Pure DI, you should begin to see the benefits of a real DI Container after reading about Lifetime Management in chapter 8. It’s possible to properly manage the lifetime of various objects in an object graph, but it requires more custom code than Object Composition. And none of that code adds any particular business value to an application. In addition to explaining the basics of Lifetime Management, chapter 8 also contains a catalog of common lifestyles. This catalog serves as a vocabulary for discussing lifestyles throughout part 4. Although you don’t have to implement any of these by hand, it’s good to know how they work.

第 3 部分的其余章节解释了 DI 的最后一个维度:拦截。在第 9 章中,我们将研究实现中经常出现的问题以基于组件的方式横切关注点。我们将使用装饰器设计模式来做到这一点。第 9 章还作为其后两章的介绍。

The remaining chapters of part 3 explain the last dimension of DI: Interception. In chapter 9, we’ll look at the frequently occurring problem of implementing Cross-Cutting Concerns in a component-based way. We’ll do this by using the Decorator design pattern. Chapter 9 also functions as an introduction to the two chapters following it.

我们将在第 10 章中介绍面向方面的编程(AOP) 范例,了解基于SOLID原则的精心设计的应用程序如何使您能够创建高度可维护的代码,而无需使用任何特殊工具。我们认为这一章是本书的高潮——许多使用早期访问程序的读者说,他们开始看到一种非常强大的软件建模方法的轮廓。

We’ll look at the Aspect-Oriented Programming (AOP) paradigm in chapter 10 and see how a careful application design, based on the SOLID principles, enables you to create highly maintainable code, without the use of any special tooling. We consider this chapter the climax of the book — this is where many readers using the early access program said they began to see the contours of a tremendously powerful way to model software.

除了应用SOLID设计原则外,还有其他方法可以实践面向方面的编程。您可以使用专门的工具,例如编译时编织和动态拦截工具,而不是使用模式和原则。这些在第 11 章中描述。

Besides applying SOLID design principles, there are other ways to practice Aspect-Oriented Programming. Instead of using patterns and principles, you can use specialized tooling such as compile-time weaving and dynamic Interception tools. These are described in chapter 11.

7

申请组成

7

Application composition

在这一章当中

In this chapter

  • 编写控制台应用程序
  • Composing console applications
  • 编写通用 Windows 编程 (UWP) 应用程序
  • Composing Universal Windows Programming (UWP) applications
  • 编写 ASP.NET Core MVC 应用程序
  • Composing ASP.NET Core MVC applications

烹制包含多道菜的美食是一项具有挑战性的工作,尤其是如果您想参与消费。你不能边吃边做饭,但很多菜都需要最后一分钟才做好。专业厨师知道如何解决其中的许多挑战。在许多交易技巧中,他们使用了就地调度的一般原则可以将其粗略地翻译为就位的一切1  凡是能提前准备好的,都好,提前准备好了。蔬菜被清洗和切碎,肉类被切碎,汤料被煮熟,烤箱被预热,工具被放置,等等。

Cooking a gourmet meal with several courses is a challenging undertaking, particularly if you want to partake in the consumption. You can’t eat and cook at the same time, yet many dishes require last-minute cooking to turn out well. Professional cooks know how to resolve many of these challenges. Amidst many tricks of the trade, they use the general principle of mise en place, which can be loosely translated to everything in place.1  Everything that can be prepared well in advance is, well, prepared in advance. Vegetables are cleaned and chopped, meats cut, stocks cooked, ovens preheated, tools laid out, and so on.

如果冰淇淋是甜点的一部分,可以在前一天制作。如果第一道菜有贻贝,可以在几个小时前清洗干净。即使是蛋黄酱这样易碎的成分,也可以提前一个小时准备好。当客人准备好时吃,只需要最后的准备:在煎肉的同时重新加热酱汁,等等。在许多情况下,这顿饭的最终组成不需要超过 5 到 10 分钟。图 7.1说明了该过程。

If ice cream is part of the dessert, it can be made the day before. If the first course contains mussels, they can be cleaned hours before. Even such a fragile component as sauce béarnaise can be prepared up to an hour before. When the guests are ready to eat, only the final preparations are necessary: reheat the sauce while frying the meat, and so on. In many cases, this final composition of the meal need not take more than 5 to 10 minutes. Figure 7.1 illustrates the process.

07-01.eps

图 7.1 Mise en place包括提前准备好膳食的所有成分,以便尽可能快速、轻松地完成膳食的最终组成。

Figure 7.1 Mise en place involves preparing all components of the meal well in advance so that the final composition of the meal can be done as quickly and effortlessly as possible.

mise en place的原则类似于使用 DI 开发松散耦合的应用程序。您可以提前编写所有必需的组件,并且只在绝对必要时才编写它们。

The principle of mise en place is similar to developing a loosely coupled application with DI. You can write all the required components well in advance and only compose them when you absolutely must.

07-02_new.eps

图 7.2 Composition Root组合了应用程序 的所有独立模块。

Figure 7.2 The Composition Root composes all the independent modules of the application.

与所有类比一样,到目前为止,我们只能采用这个类比。在烹饪中,准备和组合按时间分开,而在应用程序开发中,分离发生在模块和层之间。图 7.2显示了如何在Composition Root中组合组件。

As with all analogies, we can only take this one so far. In cooking, preparation and composition are separated by time, whereas in application development, separation occurs across modules and layers. Figure 7.2 shows how to compose the components in the Composition Root.

在运行时,首先发生的是对象组合。一旦连接了对象图,对象组合就完成了,组成部分接管了。在本章中,我们将重点关注组合根。与mise en place相比,Object Composition不会尽可能晚地发生,而是在需要集成不同模块的地方发生。

At runtime, the first thing that happens is Object Composition. As soon as the object graph is wired up, Object Composition is finished, and the constituent components take over. In this chapter, we’ll focus on the Composition Roots of several application frameworks. In contrast to mise en place, Object Composition doesn’t happen as late as possible, but in a place where integration of the different modules is required.

对象组合是 DI 的基础,也是最容易理解的部分之一。您已经知道该怎么做,因为在创建包含其他对象的对象时,您一直在组合对象。

Object Composition is the foundation of DI, and it’s one of the easiest parts to understand. You already know how to do it because you compose objects all the time when you create objects that contain other objects.

在 4.1 节中,我们介绍了何时以及如何编写应用程序的基础知识。本章不重复该信息。相反,我们希望帮助您解决在组合对象时可能出现的一些挑战。这些挑战并非源于对象组合本身,而是来自您工作的应用程序框架。这些问题往往特定于每个框架,解决方案也是如此。根据我们的经验,这些挑战构成了成功应用 DI 的一些最大障碍,因此我们将重点关注它们。这样做会使本章比前几章理论性更强,实践性更强。

In section 4.1, we covered the basics of when and how to compose applications. This chapter doesn’t repeat that information. Instead, we want to help you address some of the challenges that can arise as you compose objects. Those challenges stem not from Object Composition itself, but from the application frameworks in which you work. These issues tend to be specific to each framework, and so are the resolutions. In our experience, these challenges pose some of the greatest obstacles to successfully applying DI, so we’ll focus on them. Doing so will make the chapter less theoretical and more practical than the previous chapters.

当您可以完全控制应用程序的生命周期(就像您对命令行应用程序所做的那样)时,很容易组成应用程序的整个依赖关系层次结构。但是 .NET 中的某些框架(例如 ASP.NET Core)涉及控制反转,这有时会使应用 DI 变得更加困难。了解每个框架的接缝是为特定框架应用 DI 的关键。在本章中,我们将研究如何实现在最常见的 .NET Core 框架中实现组合根。

It’s easy to compose an application’s entire Dependency hierarchy when you have full control over the application’s lifetime (as you do with command-line applications). But some frameworks in .NET (for example, ASP.NET Core) involve Inversion of Control, which can sometimes make it more difficult to apply DI. Understanding each framework’s Seams is key to applying DI for that particular framework. In this chapter, we’ll examine how to implement Composition Roots in the most common .NET Core frameworks.

我们将以在特定框架中应用 DI 的一般介绍开始每个部分,然后是一个基于电子商务示例的广泛示例,该示例贯穿本书的大部分内容。我们将从应用 DI 的最简单的框架开始,然后逐渐研究更复杂的框架。到目前为止,最容易应用 DI 的类型是控制台应用程序,所以我们接下来将讨论这个。

We’ll begin each section with a general introduction to applying DI in a particular framework, followed by an extensive example built on the e-commerce example that runs throughout most of this book. We’ll start with the easiest framework in which to apply DI, and then gradually work through the more complex frameworks. The easiest type to apply DI to is, by far, a console application, so we’ll discuss this next.

7.1 编写控制台应用程序

7.1 Composing console applications

毫无疑问,控制台应用程序是最容易编写的应用程序类型。与大多数其他 .NET BCL 应用程序框架相反,控制台应用程序几乎不涉及控制反转。当执行到达应用程序的入口点时(通常是Main方法Program课堂上), 你是一个人。没有要订阅的特殊事件,没有要实现的接口,您可以使用的服务也很少。

A console application is, hands down, the easiest type of application to compose. Contrary to most other .NET BCL application frameworks, a console application involves virtually no Inversion of Control. When execution hits the application’s entry point (usually the Main method in the Program class), you’re on your own. There are no special events to subscribe to, no interfaces to implement, and precious few services you can use.

该类Program是一个合适的Composition Root。在它的Main方法中,您编写应用程序的模块并让它们接管。没什么,但让我们看一个例子。

The Program class is a suitable Composition Root. In its Main method, you compose the application’s modules and let them take over. There’s nothing to it, but let’s look at an example.

7.1.1 示例:使用 UpdateCurrency 程序更新货币

7.1.1 Example: Updating currencies using the UpdateCurrency program

在第 4 章中,我们研究了如何为示例电子商务应用程序提供货币转换功能。第 4.2.4 节介绍了应用从一种货币到其他货币的汇率的抽象。因为是一个接口,我们可以创建许多不同的实现,但在示例中,我们使用了一个数据库。第 4 章示例代码的目的是演示如何检索和实现货币转换,因此我们从未研究过如何更新数据库中的汇率。ICurrencyConverter ICurrencyConverter

In chapter 4, we looked at how to provide a currency conversion feature for the sample e-commerce application. Section 4.2.4 introduced the ICurrencyConverter Abstraction that applies exchange rates from one currency to other currencies. Because ICurrencyConverter is an interface, we could have created many different implementations, but in the example, we used a database. The purpose of the example code in chapter 4 was to demonstrate how to retrieve and implement currency conversion, so we never looked at how to update exchange rates in the database.

继续该示例,让我们研究如何编写一个简单的 .NET Core 控制台应用程序,使管理员或超级用户能够更新汇率,而无需直接与数据库交互。控制台应用程序与数据库对话并处理传入的命令行参数。因为这个程序的目的是更新数据库中的汇率,所以我们将其称为 UpdateCurrency。它需要两个命令行参数:

To continue the example, let’s examine how to write a simple .NET Core console application that enables an administrator or super-user to update the exchange rates without having to interact directly with the database. The console application talks to the database and processes the incoming command-line arguments. Because the purpose of this program is to update the exchange rates in the database, we’ll call it UpdateCurrency. It takes two command-line arguments:

  • 货币代码
  • The currency code
  • 从主要货币 (USD) 到此货币的汇率
  • The exchange rate from the primary currency (USD) to this currency

美元是我们系统中的主要货币,我们存储所有其他货币相对于美元的汇率。例如,美元兑欧元的汇率表示为 1 美元兑换 0.88 欧元(2018 年 12 月)。当我们想在命令行更新汇率时,它看起来像这样:

USD is the primary currency in our system, and we store all the exchange rates of other currencies relative it. For example, the exchange rate for USD to EUR is expressed as 1 USD costing 0.88 EUR (December 2018). When we want to update the exchange rate at the command line, it looks like this:

d:\> dotnet commerce\UpdateCurrency.dll EUR "0.88"
Updated: 0.88 EUR = 1 USD.

执行该程序会更新数据库并将新值写回控制台。让我们看看如何构建这样一个控制台应用程序。

Executing the program updates the database and writes the new values back to the console. Let’s look at how we build such a console application.

7.1.2 构建UpdateCurrency 程序的复合根

7.1.2 Building the Composition Root of the UpdateCurrency program

UpdateCurrency 使用控制台程序的默认入口点:类Main中的方法Program。这充当应用程序的组合根

UpdateCurrency uses the default entry point for a console program: the Main method in the Program class. This acts as the Composition Root for the application.

清单 7.1 控制台应用程序的组合根

Listing 7.1 The console application’s Composition Root

class Program
{
    static void Main(string[] args)
    {
        string connectionString =    ①  
            LoadConnectionString();    ①  

        CurrencyParser parser =    ②  
            CreateCurrencyParser(connectionString);  ②  

        ICommand command = parser.Parse(args);    ③  
        command.Execute();    ③  
    }

    static string LoadConnectionString()
    {
        var configuration = new ConfigurationBuilder()
            .SetBasePath(AppContext.BaseDirectory)
            .AddJsonFile("appsettings.json", optional: false)
            .Build();

        return configuration.GetConnectionString(
            "CommerceConnectionString");
    }

    static CurrencyParser CreateCurrencyParser(string connectionString) ...
}

该类Program的唯一职责是加载配置值,组合所有相关模块,并让组合的对象图负责功能。在此示例中,应用程序模块的组成被提取到CreateCurrencyParser方法中,而Main方法负责调用组合对象图上的方法。使用硬连线DependenciesCreateCurrencyParser组成其对象图。我们很快就会回到它来检查它是如何实现的。

The Program class’s only responsibilities are to load the configuration values, compose all relevant modules, and let the composed object graph take care of the functionality. In this example, the composition of the application’s modules is extracted to the CreateCurrencyParser method, whereas the Main method is responsible for calling methods on the composed object graph. CreateCurrencyParser composes its object graph using hardwired Dependencies. We’ll return to it shortly to examine how it’s implemented.

任何组合根应该只做四件事:加载配置值、构建对象图、调用所需的功能,以及,正如我们将在下一章讨论的那样,释放对象图。一旦完成,它就应该让开并将其余的留给被调用的实例。

Any Composition Root should only do four things: load configuration values, build the object graph, invoke the desired functionality, and, as we’ll discuss in the next chapter, release the object graph. As soon as it has done that, it should get out of the way and leave the rest to the invoked instance.

有了这个基础设施,您现在可以要求创建一个解析传入参数并最终执行相应命令的方法。这个例子使用纯 DICreateCurrencyParserCurrencyParser,但可以直接将其替换为第 4 部分中介绍的DI 容器

With this infrastructure in place, you can now ask CreateCurrencyParser to create a CurrencyParser that parses the incoming arguments and eventually executes the corresponding command. This example uses Pure DI, but it’s straightforward to replace it with a DI Container like those covered in part 4.

7.1.3 组合对象图CreateCurrencyParser

7.1.3 Composing object graphs in CreateCurrencyParser

CreateCurrencyParser方法_存在的明确目的是连接UpdateCurrency 程序的所有依赖项。以下清单显示了实现。

The CreateCurrencyParser method exists for the express purpose of wiring up all Dependencies for the UpdateCurrency program. The following listing shows the implementation.

清单 7.2 CreateCurrencyParser组成对象图的方法

Listing 7.2 CreateCurrencyParser method that composes the object graph

static CurrencyParser CreateCurrencyParser(string connectionString)
{
    IExchangeRateProvider provider =    ①  
        new SqlExchangeRateProvider(    ①  
            new CommerceContext(connectionString));    ①  
    ①  
    return new CurrencyParser(provider);    ①  
}

在此清单中,对象图相当浅。CurrencyParser班级_需要一个IExchangeRateProvider接口实例,并且您构造用于在方法中与数据库通信。SqlExchangeRateProviderCreateCurrencyParser

In this listing, the object graph is rather shallow. The CurrencyParser class requires an instance of the IExchangeRateProvider interface, and you construct SqlExchangeRateProvider for communicating with the database in the CreateCurrencyParser method.

该类CurrencyParser使用Constructor Injection,因此您将SqlExchangeRateProvider刚刚创建的实例传递给它。然后您返回CurrencyParser从该方法新创建的。如果您想知道,这里是 的构造函数签名CurrencyParser

The CurrencyParser class uses Constructor Injection, so you pass it the SqlExchangeRateProvider instance that was just created. You then return the newly created CurrencyParser from the method. In case you’re wondering, here’s the constructor signature of CurrencyParser:

public CurrencyParser(IExchangeRateProvider exchangeRateProvider)

回想一下,这是一个由. 作为Composition Root的一部分,包含从到的硬编码映射。然而,其余代码仍然是松散耦合的,因为它只使用AbstractionIExchangeRateProviderSqlExchangeRateProviderCreateCurrencyParserIExchangeRateProviderSqlExchangeRateProvider

Recall that IExchangeRateProvider is an interface that’s implemented by SqlExchangeRateProvider. As part of the Composition Root, CreateCurrencyParser contains a hard-coded mapping from IExchangeRateProvider to SqlExchangeRateProvider. The rest of the code, however, remains loosely coupled, because it consumes only the Abstraction.

这个例子看似简单,但它组合了来自三个不同应用层的类型。让我们简要检查一下这些层在这个例子中是如何相互作用的。

This example may seem simple, but it composes types from three different application layers. Let’s briefly examine how these layers interact in this example.

7.1.4 仔细查看 UpdateCurrency 的分层

7.1.4 A closer look at UpdateCurrency’s layering

Composition Root是所有层的组件连接在一起的地方。入口点和组合根构成可执行文件的唯一代码。如图 7.3所示,所有实现都委托给较低层。

The Composition Root is where components from all layers are wired together. The entry point and the Composition Root constitute the only code of the executable. All implementation is delegated to lower layers, as figure 7.3 illustrates.

07-03_resized.eps

图 7.3 UpdateCurrency 应用程序的组件组成

Figure 7.3 Component composition of the UpdateCurrency application

图 7.3中的图表可能看起来很复杂,但它几乎代表了控制台应用程序的整个代码库。大多数应用程序逻辑包括解析输入参数和根据输入选择正确的命令。所有这一切都发生在应用服务层,它只通过IExchangeRateProvider接口和域直接与领域层对话。Currency类直接与领域层对话.

The diagram in figure 7.3 may look complicated, but it represents almost the entire code base of the console application. Most of the application logic consists of parsing the input arguments and choosing the correct command based on the input. All this takes place in the application services layer, which only talks directly with the domain layer via the IExchangeRateProvider interface and the Currency class.

IExchangeRateProviderCurrencyParserComposition Root注入,随后用作抽象工厂来创建Currency由. 数据访问层提供域抽象的基于 SQL Server 的实现。尽管没有其他应用程序类直接与这些实现对话,但将抽象映射到具体类。UpdateCurrencyCommandCreateCurrencyParser

IExchangeRateProvider is injected into CurrencyParser by the Composition Root and is subsequently used as an Abstract Factory to create a Currency instance used by UpdateCurrencyCommand. The data access layer supplies the SQL Server–based implementations of the domain Abstractions. Although none of the other application classes talk directly to those implementations, CreateCurrencyParser maps the Abstractions to the concrete classes.

将 DI 与控制台应用程序一起使用很容易,因为实际上不涉及外部控制反转。.NET Framework 启动进程并将控制权交给Main方法。这类似于使用通用 Windows 编程 (UWP),它允许没有任何接缝的对象组合

Using DI with a console application is easy because there’s virtually no external Inversion of Control involved. The .NET Framework spins up the process and hands control to the Main method. This is similar to working with Universal Windows Programming (UWP), which allows Object Composition without any Seams.

7.2 编写 UWP 应用程序

7.2 Composing UWP applications

编写 UWP 应用程序几乎与编写控制台应用程序一样简单。在本节中,我们将实现一个小型 UWP 应用程序,用于使用 Model-View-ViewModel 管理电子商务应用程序的产品(MVVM) 图案。我们将了解放置Composition Root的位置、如何构造和初始化视图模型、如何将视图绑定到相应的视图模型,以及如何确保我们可以从一个页面导航到下一个页面。

Composing a UWP application is almost as easy as composing a console application. In this section, we’ll implement a small UWP application for managing products of the e-commerce application using the Model-View-ViewModel (MVVM) pattern. We’ll take a look at where to place the Composition Root, how to construct and initialize view models, how to bind views to their corresponding view models, and how to ensure we can navigate from one page to the next.

UWP 应用程序的入口点相当简单,尽管它不提供Seams明确以启用 DI 为目标,您可以轻松地以您喜欢的任何方式编写应用程序。

A UWP application’s entry point is fairly uncomplicated, and although it doesn’t provide Seams explicitly targeted at enabling DI, you can easily compose an application in any way you prefer.

在本节中,我们不会教授 UWP 本身。假设您具备有关构建 UWP 应用程序的基本知识。3个 

In this section, we won’t be teaching UWP itself. Basic knowledge about building UWP applications is assumed.3 

7.2.1 UWP组成

7.2.1 UWP composition

UWP 应用程序的入口点在其App类中定义。与 UWP 中的大多数其他类一样,此类分为两个文件:App.xaml 和 App.xaml.cs。您可以在 App.xaml.cs 中定义应用程序启动时发生的情况。

A UWP application’s entry point is defined in its App class. As with most other classes in UWP, this class is split into two files: App.xaml and App.xaml.cs. You define what happens at application startup in the App.xaml.cs.

当你在 Visual Studio 中创建一个新的 UWP 项目时,App.xaml.cs 文件定义了一个OnLaunched方法来定义应用程序启动时显示哪个页面;在这种情况下,MainPage

When you create a new UWP project in Visual Studio, the App.xaml.cs file defines an OnLaunched method that defines which page is shown when the application starts; in this case, MainPage.

清单 7.3 OnLaunched方法App.xaml.cs 文件的

Listing 7.3 OnLaunched method of the App.xaml.cs file

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    ...

    rootFrame.Navigate(typeof(MainPage), e.Arguments);    ①  

    ...
}

OnLaunched方法类似于控制台应用程序的Main方法——它是您的应用程序的入口点。该类App成为应用程序的Composition Root。您可以使用DI ContainerPure DI来组合页面;下一个示例使用Pure DI

The OnLaunched method is similar to a console application’s Main method — it’s the entry point for your application. The App class becomes the application’s Composition Root. You can use a DI Container or Pure DI to compose the page; the next example uses Pure DI.

7.2.2 示例:连接产品管理富客户端

7.2.2 Example: Wiring up a product-management rich client

上一节中的示例创建了用于设置汇率的商务控制台应用程序。在此示例中,你将创建一个使你能够管理产品的 UWP 应用程序。图 7.4 和 7.5 显示了该应用程序的屏幕截图。

The example in the previous section created our commerce console application for setting exchange rates. In this example, you’ll create a UWP application that enables you to manage products. Figures 7.4 and 7.5 show screen captures of this application.

07-04.tif

图 7.4 产品管理的主页是产品列表。您可以通过点击一行来编辑或删除产品,也可以通过点击添加产品来添加新产品。

Figure 7.4 Product Management’s main page is a list of products. You can edit or delete products by tapping on a row, or you can add a new product by tapping Add Product.

07-05.tif

图 7.5 产品管理的产品编辑页面让您可以更改产品名称和以美元为单位的单价。该应用程序使用 UWP 的默认命令栏。

Figure 7.5 Product Management’s product-edit page lets you change the product name and unit price in dollars. The application makes use of UWP’s default command bar.

整个应用程序使用 MVVM 方法实现,包含图 7.6所示的四个层。我们将逻辑最多的部分与​​其他模块隔离开来;在这种情况下,这就是表示逻辑。UWP 客户端层是一个薄层,除了定义 UI 和将实现委派给其他模块外,它几乎不做任何事情。

The entire application is implemented using the MVVM approach and contains the four layers shown in figure 7.6. We keep the part with the most logic isolated from the other modules; in this case, that’s the presentation logic. The UWP client layer is a thin layer that does little apart from defining the UI and delegating implementation to the other modules.

07-06_resized.eps

图 7.6 产品管理富客户端应用程序的四个不同组件

Figure 7.6 The four distinct assemblies of the product-management rich client application

图 7.6中的图表与您在前面章节中看到的类似,只是增加了表示逻辑层。数据访问层可以直接连接到数据库,就像我们在电子商务 Web 应用程序中所做的那样,或者它可以连接到产品管理 Web 服务。信息的存储方式与表示逻辑层无关,因此我们不会在本章中详细介绍。

The diagram in figure 7.6 is similar to what you’ve seen in previous chapters, with the addition of a presentation logic layer. The data access layer can directly connect to a database, as we did in the e-commerce web application, or it can connect to a product-management web service. How the information is stored isn’t that relevant where the presentation logic layer is concerned, so we won’t go into details about that in this chapter.

使用 MVVM,您可以将 ViewModel 分配给页面的DataContext属性,数据绑定和数据模板引擎负责在您启动新 ViewModel 或更改现有 ViewModel 中的数据时正确呈现数据。但是,在创建第一个 之前ViewModel,您需要定义一些结构,使 ViewModel 能够导航到其他 ViewModel。同样,要使用向用户显示页面时所需的运行时数据初始化 ViewModel,您必须让 ViewModel 实现自定义接口。在进入应用程序的实质之前,以下部分解决了这些问题:MainViewModel.

With MVVM, you assign a ViewModel to a page’s DataContext property, and the data-binding and data-templating engines take care of presenting the data correctly as you spin up new ViewModels or change the data in the existing ViewModels. Before you can create the first ViewModel, however, you need to define some constructs that enable ViewModels to navigate to other ViewModels. Likewise, for a ViewModel to be initialized with the runtime data required when a page is shown to the user, you must let the ViewModels implement a custom interface. The following section addresses these concerns before getting to the meat of the application: the MainViewModel.

注入依赖MainViewModel

Injecting Dependencies into the MainViewModel

MainPage仅包含 XAML 标记,不包含自定义代码隐藏。相反,它使用数据绑定来显示数据和处理用户命令。要启用此功能,您必须为其MainViewModel属性DataContext分配一个. 然而,这是Property Injection的一种形式。我们想改用构造函数注入。为了实现这一点,我们MainPage使用一个重载的构造函数删除了 的默认构造函数,该构造函数接受MainViewModel作为参数,其中构造函数在内部分配该DataContext属性:

MainPage contains only XAML markup and no custom code-behind. Instead, it uses data binding to display data and handle user commands. To enable this, you must assign a MainViewModel to its DataContext property. This, however, is a form of Property Injection. We'd like to use Constructor Injection instead. To allow this, we remove the MainPage’s default constructor with an overloaded constructor that accepts the MainViewModel as an argument, where the constructor internally assigns that DataContext property:

public sealed partial class MainPage : Page
{
    public MainPage(MainViewModel vm)
    {
        this.InitializeComponent();

        this.DataContext = vm;
    }
}

MainViewModel公开数据,例如产品列表,以及创建、更新或删除产品的命令。启用此功能取决于提供对产品目录的访问的服务:IProductRepository 抽象。除此之外IProductRepositoryMainViewModel还需要一个它可以使用的服务控制其窗口环境,例如导航到其他页面。这另一个依赖项称为INavigationService

MainViewModel exposes data, such as the list of products, as well as commands to create, update, or delete a product. Enabling this functionality depends on a service that provides access to the product catalog: the IProductRepository Abstraction. Apart from IProductRepository, MainViewModel also needs a service that it can use to control its windowing environment, such as navigating to other pages. This other Dependency is called INavigationService:

public interface INavigationService
{
    void NavigateTo<TViewModel>(Action whenDone = null, object model = null)
        where TViewModel : IViewModel;
}

NavigateTo方法是通用的,因此它需要导航到的 ViewModel 的类型必须作为其通用类型参数提供。方法参数由导航服务传递给创建的 ViewModel。为此,ViewModel 必须实现IViewModel. 为此,该NavigateTo方法指定泛型类型约束where TViewModel : IViewModel5  以下代码片段显示IViewModel

The NavigateTo method is generic, so the type of ViewModel that it needs to navigate to must be supplied as its generic type argument. The method arguments are passed by the navigation service to the created ViewModel. For this to work, a ViewModel must implement IViewModel. For this reason, the NavigateTo method specifies the generic type constraint where TViewModel : IViewModel.5  The following code snippet shows IViewModel:

public interface IViewModel
{
    void Initialize(Action whenDone, object model);    ①  
}

Initialize方法_INavigationService.NavigateTo包含与方法相同的参数. 导航服务将Initialize在构造的 ViewModel 上调用。表示 ViewModel 需要初始化的model数据,比如一个Product. whenDone行动_允许原始 ViewModel 在用户退出此 ViewModel 时得到通知,我们将在稍后讨论。

The Initialize method contains the same arguments as the INavigationService.NavigateTo method. The navigation service will invoke Initialize on a constructed ViewModel. The model represents the data that the ViewModel needs to initialize, such as a Product. The whenDone action allows the originating ViewModel to get notified when the user exits this ViewModel, as we’ll discuss shortly.

使用之前的接口定义,您现在可以为 构造一个 ViewModel MainPage。以下列表显示MainViewModel了它的全部荣耀。

Using the previous interface definitions, you can now construct a ViewModel for MainPage. The following listing shows MainViewModel in its full glory.

MainViewModel清单7.4

Listing 7.4 The MainViewModel class

public class MainViewModel : IViewModel,
    INotifyPropertyChanged    ①  
{
    private readonly INavigationService navigator;
    private readonly IProductRepository repository;

    public MainViewModel(
        INavigationService navigator,
        IProductRepository repository)
    {
        this.navigator = navigator;
        this.repository = repository;

        this.AddProductCommand =
            new RelayCommand(this.AddProduct);
        this.EditProductCommand =
            new RelayCommand(this.EditProduct);
    }

    public IEnumerable<Product> Model { get; set; }    ②  
    public ICommand AddProductCommand { get; }    ②  
    public ICommand EditProductCommand { get; }    ②  

    public event PropertyChangedEventHandler
        PropertyChanged = (s, e) => { };

    public void Initialize(    ③  
        object model, Action whenDone)
    {
        this.Model = this.repository.GetAll();
        this.PropertyChanged.Invoke(this,    ④  
            new PropertyChangedEventArgs("Model"));
    }

    private void AddProduct()    ⑤  
    {    ⑤  
        this.navigator.NavigateTo<NewProductViewModel>(  ⑤  
            whenDone: this.GoBack);    ⑤  
    }

    private void EditProduct(object product)    ⑥  
    {
       this.navigator.NavigateTo<EditProductViewModel>(
            whenDone: this.GoBack,
            model: product;    ⑦  
    }

    private void GoBack()
    {
        this.navigator.NavigateTo<MainViewModel>();
    }
}

命令方法和都指示导航到相应 ViewModel 的页面。在 的情况下,这对应于。该方法随附一个委托,当用户在该页面上完成工作时将调用该委托。这导致调用的方法AddProductEditProductINavigationServiceAddProductNewProductViewModelNavigateToNewProductViewModelMainViewModelGoBack,这会将应用程序导航回。为了描绘一幅完整的图画,清单 7.5显示了XAML 定义的简化版本以及 XAML 如何绑定到、和的属性。MainViewModelMainPageModelEditProductCommandAddProductCommandMainViewModel

The command methods, AddProduct and EditProduct, both instruct INavigationService to navigate to the page for the corresponding ViewModel. In the case of AddProduct, this corresponds to NewProductViewModel. The NavigateTo method is supplied with a delegate that’ll be invoked by NewProductViewModel when the user finishes working on that page. This results in invoking the MainViewModel’s GoBack method, which will navigate the application back to MainViewModel. To paint a complete picture, listing 7.5 shows a simplified version of the MainPage XAML definition and how the XAML is bound to the Model, EditProductCommand, and AddProductCommand properties of MainViewModel.

清单 7.5 XAML 的MainPage

Listing 7.5 XAML of MainPage

<Page x:Class="Ploeh.Samples.ProductManagement.UWPClient.MainPage"
    xmlns:commands="using:ProductManagement.PresentationLogic.UICommands"
    ...>
    <Grid>
        <Grid.RowDefinitions>
            ...
        </Grid.RowDefinitions>

        <GridView ItemsSource="{Binding Model}"
            commands:ItemClickCommand.Command="{Binding EditProductCommand}"
            IsItemClickEnabled="True">
            <GridView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="2*"/>
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <StackPanel Grid.Column="0">
                            <TextBlock Text="{Binding Name}" />
                        </StackPanel>
                        <StackPanel Grid.Column="1">
                            <TextBlock Text="{Binding UnitPrice}" />
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>

        <CommandBar Grid.Row="5" Grid.ColumnSpan="3" Grid.Column="0">
            <AppBarToggleButton Icon="Add" Label="Add product"
                Command="{Binding AddProductCommand}" />
        </CommandBar>
    </Grid>
</Page>

尽管以前的 XAML 使用较旧的Binding标记扩展,但作为 UWP 开发人员,您可能习惯于使用较新的x:Bind标记扩展。x:Bind提供编译时支持,但需要在编译时固定类型,通常在视图的代码隐藏类中定义。因为您绑定到存储在无类型DataContext属性中的 ViewModel,所以您失去了编译时支持,因此需要回退到Binding标记扩展. 6个 

Although the previous XAML makes use of the older Binding markup extension, as a UWP developer, you might be used to using the newer x:Bind markup extension. x:Bind gives compile-time support, but requires types to be fixed at compile time, typically defined in the view’s code-behind class. Because you bind to a ViewModel that’s stored in the untyped DataContext property, you lose compile-time support and, therefore, need to fall back to the Binding markup extension.6 

MainPageXAML 中的两个主要元素是 aGridView和 a CommandBarGridView用于显示可用产品并绑定到和属性;它绑定到的元素的和属性。这会显示一个通用功能区,其中包含允许用户调用的操作。绑定到属性ModelEditProductCommandDataTemplateNameUnitPriceModelProductCommandBarCommandBarAddProductCommand. 使用 和 的定义MainViewModelMainPage您现在可以开始连接应用程序。

The two main elements in the MainPage XAML are a GridView and a CommandBar. The GridView is used to display the available products and bind to both the Model and EditProductCommand properties; its DataTemplate binds to the Name and UnitPrice properties of the Model’s Product elements. The CommandBar displays a generic ribbon with operations that the user is allowed to invoke. The CommandBar binds to the AddProductCommand property. With the definitions of MainViewModel and MainPage, you can now start wiring up the application.

接线MainViewModel

Wiring up MainViewModel

在接线之前,让我们看一下这个依赖关系MainViewModel中涉及的所有类。图 7.7显示了应用程序的图形,从 开始。MainPage

Before wiring up MainViewModel, let’s take a look at all the classes involved in this Dependency graph. Figure 7.7 shows the graph for the application, starting with MainPage.

现在您已经确定了应用程序的所有构建块,您可以对其进行组合。为此,您必须同时创建 aMainViewModel和 a MainPage,然后将 ViewModel 注入到MainPage的构造函数中. 要连接起来,您必须将它与它的依赖项组合起来:MainViewModel

Now that you’ve identified all the building blocks of the application, you can compose it. To do this, you must create both a MainViewModel and a MainPage, and then inject the ViewModel to the MainPage’s constructor. To wire up MainViewModel, you have to compose it with its Dependencies:

IViewModel vm = new MainViewModel(navigationService, productRepository);
Page view = new MainPage(vm);

正如您在清单 7.3中看到的,默认的 Visual Studio 模板调用Frame.Navigate(Type). Navigate方法_Page代表您创建一个新实例并将该页面显示给用户。无法向 提供Page实例Navigate,但您可以通过手动将创建的页面分配给Content属性来解决此问题应用程序的主要部分Frame

As you saw in listing 7.3, the default Visual Studio template calls Frame.Navigate(Type). The Navigate method creates a new Page instance on your behalf and shows that page to the user. There’s no way to supply a Page instance to Navigate, but you can work around this by manually assigning the page created to the Content property of the application’s main Frame:

var frame = (Frame)Window.Current.Content;
frame.Content = view;

因为这些是将应用程序粘合在一起的重要部分,所以这正是您将在Composition Root中执行的操作。

Because these are the important pieces to glue the application together, this is exactly what you’ll do in the Composition Root.

07-07_resized.eps

图 7.7 产品管理富客户端的依赖关系图

Figure 7.7 Dependency graph of the product-management rich client

7.2.3在 UWP 应用程序中实现Composition Root

7.2.3 Implementing the Composition Root in the UWP application

有很多方法可以创建Composition Root。对于此示例,我们选择将导航逻辑和 View/ViewModel 对的构造放在 App.xaml.cs 文件中,以使示例相对简洁。应用程序的Composition Root如图 7.8所示。

There are many ways to create the Composition Root. For this example, we chose to place both the navigation logic and the construction of View/ViewModel pairs inside the App.xaml.cs file to keep the example relatively succinct. The application’s Composition Root is displayed in figure 7.8 .

07-08_resized.eps

图 7.8 产品管理富客户端的组合根

Figure 7.8 The product-management rich client’s Composition Root

下一个清单显示了我们的Composition Root的实际应用。

The next listing shows our Composition Root in action.

清单 7.6 产品管理App包含合成根

Listing 7.6 The product-management App class containing the Composition Root

public sealed partial class App : Application, INavigationService
{
    protected override void OnLaunched(    ①  
        LaunchActivatedEventArgs e)
    {
        if (Window.Current.Content == null)
        {
            Window.Current.Content = new Frame();    ②  
            Window.Current.Activate();
            this.NavigateTo<MainViewModel>(null, null);  ③  
        }
    }

    public void NavigateTo<TViewModel>(
        Action whenDone, object model)
        where TViewModel : IViewModel
    {
        var page = this.CreatePage(typeof(TViewModel));  ④  
        var viewModel = (IViewModel)page.DataContext;    ④  
    ④  
        viewModel.Initialize(whenDone, model);    ④  
    ④  
        var frame = (Frame)Window.Current.Content;    ④  
        frame.Content = page;    ④  
    }

    private Page CreatePage(Type vmType)
    {
        var repository = new WcfProductRepository();    ⑤  
    ⑤  
        if (vmType == typeof(MainViewModel))    ⑤  
        {  ⑤  
            return new MainPage(    ⑤  
                new MainViewModel(this, repository));    ⑤  
        }  ⑤  
        else if (vmType == typeof(EditProductViewModel))  ⑤  
        {  ⑤  
            return new EditProductPage(    ⑤  
                new EditProductViewModel(repository));  ⑤  
        }  ⑤  
        else if (vmType == typeof(NewProductViewModel))  ⑤  
        {  ⑤  
            return new NewProductPage(    ⑤  
                new NewProductViewModel(repository));    ⑤  
        {  ⑤  
        else
        {
            throw new Exception(“Unknown view model.”);
        }
    ...
}

工厂方法类似于我们在 4.1 节中讨论的Composition Root示例。它由一大串语句组成,以相应地构建正确的对。CreatePageelse if

The CreatePage factory method is similar to the Composition Root examples we discussed in section 4.1. It consists of a big list of else if statements to construct the correct pair accordingly.

UWP 为Composition Root提供了一个简单的位置。您需要做的就是删除对Frame.Navigate(Type)from的调用OnLaunchedFrame.Content使用手动创建的Page类进行设置,该类由 ViewModel 及其Dependencies组成。

UWP offers a simple place for a Composition Root. All you need to do is remove the call to Frame.Navigate(Type) from OnLaunched and set Frame.Content with a manually created Page class, which is composed using a ViewModel and its Dependencies.

在大多数其他框架中,存在更高程度的控制反转,这意味着我们需要能够识别正确的扩展点以连接所需的对象图。一种这样的框架是 ASP.NET Core MVC。

In most other frameworks, there’s a higher degree of Inversion of Control, which means we need to be able to identify the correct extensibility points to wire up the desired object graph. One such framework is ASP.NET Core MVC.

7.3 编写 ASP.NET Core MVC 应用程序

7.3 Composing ASP.NET Core MVC applications

ASP.NET Core MVC 是为支持 DI 而构建和设计的。它带有自己的内部组合引擎,您可以使用它来构建自己的组件;不过,正如您将看到的,它并不强制您的应用程序组件使用DI 容器。您可以使用纯 DI或您喜欢的任何DI 容器。7 

ASP.NET Core MVC was built and designed to support DI. It comes with its own internal composition engine that you can use to build up its own components; although, as you’ll see, it doesn’t enforce the use of a DI Container for your application components. You can use Pure DI or whichever DI Container you like.7 

在本节中,您将学习如何使用 ASP.NET Core MVC 的主要扩展点,它允许您插入逻辑来组合控制器类及其依赖项。本节从 DI对象组合的角度来看 ASP.NET Core MVC 。然而,构建 ASP.NET Core 应用程序的内容远远超过我们在一章中可以解决的问题。如果您想了解有关如何使用 ASP.NET Core 构建应用程序的更多信息,请查看 Andrew Lock 的ASP.NET Core 实战(Manning,2018 年)。之后,我们将看看如何插入需要Dependencies的自定义中间件。

In this section, you’ll learn how to use the main extensibility point of ASP.NET Core MVC, which allows you to plug in your logic for composing controller classes with their Dependencies. This section looks at ASP.NET Core MVC from the perspective of DI Object Composition. There’s a lot more to building ASP.NET Core applications than we can address in a single chapter, however. If you want to learn more about how to build applications with ASP.NET Core, take a look at Andrew Lock’s ASP.NET Core in Action (Manning, 2018). After that, we’ll take a look at how to plug in custom middleware that requires Dependencies.

与在应用程序框架中练习 DI 一样,应用它的关键是找到正确的扩展点。在 ASP.NET Core MVC 中,这是一个名为. 图 7.9说明了它是如何融入框架的。IControllerActivator

As is always the case with practicing DI in an application framework, the key to applying it is finding the correct extensibility point. In ASP.NET Core MVC, this is an interface called IControllerActivator. Figure 7.9 illustrates how it fits into the framework.

07-09_resized.eps

图 7.9 ASP.NET Core MVC 请求管道

Figure 7.9 The ASP.NET Core MVC request pipeline

控制器是 ASP.NET Core MVC 的核心。他们处理请求并确定如何响应。如果您需要查询数据库、验证和保存传入数据、调用域逻辑等,您可以从控制器启动此类操作。控制器不应该自己做这些事情,而是将工作委托给适当的Dependencies。这就是 DI 的用武之地。

Controllers are central to ASP.NET Core MVC. They handle requests and determine how to respond. If you need to query a database, validate and save incoming data, invoke domain logic, and so on, you initiate such actions from a controller. A controller shouldn’t do such things itself, but rather delegate the work to the appropriate Dependencies. This is where DI comes in.

您希望能够为给定的控制器类提供依赖项,最好是通过构造函数注入。这可以通过自定义IControllerActivator.

You want to be able to supply Dependencies to a given controller class, ideally by Constructor Injection. This is possible with a custom IControllerActivator.

7.3.1 创建自定义控制器激活器

7.3.1 Creating a custom controller activator

创建自定义控制器激活器并不是特别困难。它需要你实现IControllerActivator接口:

Creating a custom controller activator isn’t particularly difficult. It requires you to implement the IControllerActivator interface:

public interface IControllerActivator
{
    object Create(ControllerContext context);
    void Release(ControllerContext context, object controller);
}

Create方法_提供一个ControllerContext包含HttpContext控制器类型等信息的 。这是您有机会连接所有必需的依赖项并在返回实例之前将它们提供给控制器的方法。稍后您将看到一个示例。

The Create method provides a ControllerContext that contains information such as the HttpContext and the controller type. This is the method where you get the chance to wire up all required Dependencies and supply them to the controller before returning the instance. You’ll see an example in a moment.

如果您创建了任何需要显式处理的资源,则可以在Release方法时执行此操作叫做。我们将在下一章中详细介绍有关发布组件的信息。确保释放依赖HttpContext.Response.RegisterForDispose项的更实用方法是使用方法将它们添加到可释放请求对象列表中. 尽管实现自定义控制器激活器是困难的部分,但除非我们将其告知 ASP.NET Core MVC,否则不会使用它。

If you created any resources that need to be explicitly disposed of, you can do that when the Release method is called. We’ll go into further details about releasing components in the next chapter. A more practical way to ensure that Dependencies are disposed of is to add them to the list of disposable request objects using the HttpContext.Response.RegisterForDispose method. Although implementing a custom controller activator is the hard part, it won’t be used unless we tell ASP.NET Core MVC about it.

在 ASP.NET Core 中使用自定义控制器激活器

Using a custom controller activator in ASP.NET Core

可以将自定义控制器激活器添加为应用程序启动序列的一部分——通常在Startup类中. 它们通过调用实例来AddSingleton<IControllerActivator>使用。IServiceCollection下一个清单显示了Startup示例电子商务应用程序中的类。

A custom controller activator can be added as part of the application startup sequence — usually in the Startup class. They’re used by calling AddSingleton<IControllerActivator> on the IServiceCollection instance. The next listing shows the Startup class from the sample e-commerce application.

清单 7.7 Commerce 应用程序的Startup

Listing 7.7 Commerce application’s Startup class

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        this.Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        var controllerActivator = new CommerceControllerActivator(
            Configuration.GetConnectionString("CommerceConnectionString"));

        services.AddSingleton<IControllerActivator>(controllerActivator);
    }

    public void Configure(ApplicationBuilder app, IHostingEnvironment env)
    {
        ...
    }
}

此清单创建自定义. 通过使用 将其添加到已知服务列表,您可以确保控制器的创建被您的自定义控制器激活器拦截。如果这段代码看起来有点眼熟,那是因为您在 4.1.3 节中看到了类似的东西。那时,我们承诺在第 7 章中向您展示如何实现自定义控制器激活器,您知道什么?这是第7章。CommerceControllerActivatorAddSingleton

This listing creates a new instance of the custom CommerceControllerActivator. By adding it to the list of known services using AddSingleton, you ensure the creation of controllers is Intercepted by your custom controller activator. If this code looks vaguely familiar, it’s because you saw something similar in section 4.1.3. Back then, we promised to show you how to implement a custom controller activator in chapter 7, and what do you know? This is chapter 7.

示例:实施CommerceControllerActivator

Example: implementing the CommerceControllerActivator

您可能还记得第 2 章和第 3 章,电子商务示例应用程序向网站访问者展示了产品列表及其价格。在 6.2 节中,我们添加了一项功能,允许用户计算两个位置之间的路线。虽然我们已经展示了Composition Root的几个片段,但我们没有展示完整的示例。连同listing 7.7Startupclass,listing 7.8CommerceControllerActivatorclass显示完整的Composition Root

As you might recall from chapters 2 and 3, the e-commerce sample application presents the visitor of the website with a list of products and their prices. In section 6.2, we added a feature that allowed users to calculate a route between two locations. Although we’ve shown several snippets of the Composition Root, we didn’t show a complete example. Together with listing 7.7’s Startup class, listing 7.8’s CommerceControllerActivator class shows a complete Composition Root.

电子商务示例应用程序需要一个自定义控制器激活器来连接控制器及其所需的Dependencies。虽然整个对象图相当深,但从控制器本身的角度来看,所有直接依赖的联合小到两个项目(图 7.10)。

The e-commerce sample application needs a custom controller activator to wire up controllers with their required Dependencies. Although the entire object graph is considerably deeper, from the perspective of the controllers themselves, the union of all immediate Dependencies is as small as two items (figure 7.10).

07-10.eps

图 7.10 示例应用程序中的两个控制器及其依赖关系

Figure 7.10 Two controllers in the sample application with their Dependencies

下面的清单显示了一个CommerceControllerActivatorHomeControllerRouteController及其Dependencies组成的.

The following listing shows a CommerceControllerActivator that composes both HomeController and RouteController with their Dependencies.

清单 7.8 创建控制器使用自定义控制器激活器

Listing 7.8 Creating controllers using a custom controller activator

public class CommerceControllerActivator : IControllerActivator
{
    private readonly string connectionString;

    public CommerceControllerActivator(string connectionString)
    {
        this.connectionString = connectionString;
    }

   public object Create(ControllerContext context)
    {
        Type type = context.ActionDescriptor    ①  
            .ControllerTypeInfo.AsType();    ①  

        if (type == typeof(HomeController))    ②  
        {    ②  
            return this.CreateHomeController();    ②  
        }    ②  
        else if (type == typeof(RouteController))    ②  
        {    ②  
            return this.CreateRouteController();    ②  
        }
        else
        {
            throw new Exception("Unknown controller " + type.Name);
        }
    }

    private HomeController CreateHomeController()
    {
        return new HomeController(    ③  
            new ProductService(    ③  
                new SqlProductRepository(    ③  
                    new CommerceContext(    ③  
                        this.connectionString)),    ③  
                new AspNetUserContextAdapter()));    ③  
    }

    private RouteController CreateRouteController()
    {
        var routeAlgorithms = ...;    ③  
        return new RouteController(    ③  
            new RouteCalculator(routeAlgorithms));    ③  
    }

    public void Release(    ④  
        ControllerContext context, object controller)    ④  
    {    ④  
    }    ④  
}

当一个实例在 中注册时,它会正确地创建具有所需依赖项的所有请求的控制器。除了控制器之外,其他经常需要使用 DI 的常见组件是 ASP.NET Core 所说的中间件。CommerceControllerActivatorStartup

When a CommerceControllerActivator instance is registered in Startup, it correctly creates all requested controllers with the required Dependencies. Besides controllers, other common components that often require the use of DI are what ASP.NET Core calls middleware.

7.3.2 使用Pure DI构建自定义中间件组件

7.3.2 Constructing custom middleware components using Pure DI

ASP.NET Core 使得在请求管道中插入额外行为变得相对容易。这种行为会影响请求和响应。在 ASP.NET Core 中,这些对请求管道的扩展称为中间件。将中间件连接到请求管道的典型用法是通过Use扩展方法:

ASP.NET Core makes it relatively easy to plug in extra behavior in the request pipeline. Such behavior can influence the request and response. In ASP.NET Core, these extensions to the request pipeline are called middleware. A typical use of hooking up middleware to the request pipeline is through the Use extension method:

var logger =
    loggerFactory.CreateLogger("Middleware");    ①  

app.Use(async (context, next) =>    ②  
{
    logger.LogInformation("Request started");    ③  

    await next();    ④  

    logger.LogInformation("Request ended");    ⑤  
});

然而,更常见的是,在请求的主要逻辑运行之前或之后需要完成更多工作。因此,您可能希望将此类中间件逻辑提取到它自己的类中。这会阻止你的Startup班级免于混乱,并让您有机会对该逻辑进行单元测试,如果您愿意的话。您可以将我们之前的Uselambda 的主体提取到一个Invoke方法中在一个新创建的LoggingMiddleware班级:

More often, however, more work needs to be done prior to or after the request’s main logic runs. You might therefore want to extract such middleware logic into its own class. This prevents your Startup class from being cluttered and gives you the opportunity to unit test this logic, should you want to do so. You can extract the body of our previous Use lambda to an Invoke method on a newly created LoggingMiddleware class:

public class LoggingMiddleware
{
    private readonly ILogger logger;

    public LoggingMiddleware(ILogger logger)    ①  
    {
        this.logger = logger;
    }

    public async Task Invoke(    ②  
        HttpContext context, Func<Task> next)
    {
        this.logger.LogInformation("Request started");
        await next();
        this.logger.LogInformation("Request ended");
    }
}

现在将中间件逻辑移到LoggingMiddleware类中,Startup配置可以最小化为以下代码:

With the middleware logic now moved into the LoggingMiddleware class, the Startup configuration can be minimized to the following code:

var logger = loggerFactory.CreateLogger("Middleware");

app.Use(async (context, next) =>
{
    var middleware = new LoggingMiddleware(logger);    ①  

    await middleware.Invoke(context, next);    ②  
});

ASP.NET Core MVC 的伟大之处在于它在设计时就考虑到了 DI,因此,在大多数情况下,您只需要知道并使用单个扩展点即可为应用程序启用 DI。对象组合是 DI 的三个重要维度之一(其他维度是生命周期管理拦截)。

The great thing about ASP.NET Core MVC is that it was designed with DI in mind, so, for the most part, you only need to know and use a single extensibility point to enable DI for an application. Object Composition is one of three important dimensions of DI (the others being Lifetime Management and Interception).

在本章中,我们向您展示了如何在各种不同的环境中使用松散耦合的模块来组合应用程序。一些框架实际上使它变得容易。当您编写控制台应用程序和 Windows 客户端(例如 UWP)时,您或多或少可以直接控制应用程序入口点发生的事情。这为您提供了一个独特且易于实现的Composition Root。其他框架,例如 ASP.NET Core,让您的工作更辛苦一些,但它们仍然提供Seams,您可以使用它来定义应用程序的组成方式。ASP.NET Core 在设计时考虑到了 DI,因此编写应用程序就像实现自定义IControllerActivator并将其添加到框架一样简单。

In this chapter, we’ve shown you how to compose applications from loosely coupled modules in a variety of different environments. Some frameworks actually make it easy. When you’re writing console applications and Windows clients (such as UWP), you’re more or less in direct control of what’s happening at the application’s entry point. This provides you with a distinct and easily implemented Composition Root. Other frameworks, such as ASP.NET Core, make you work a little harder, but they still provide Seams you can use to define how the application should be composed. ASP.NET Core was designed with DI in mind, so composing an application is as easy as implementing a custom IControllerActivator and adding it to the framework.

没有Object Composition ,就没有 DI,但是当我们将对象的创建从消费类中移出时,您可能还没有完全意识到对象生命周期的含义。您可能会发现外部调用者(通常是DI Container)创建新的依赖实例是不言自明的 ——但是注入的实例何时被释放?如果外部调用者不是每次都创建新实例,而是给您一个现有实例怎么办?这些是下一章的主题。

Without Object Composition, there’s no DI, but you may not yet have fully realized the implications for Object Lifetime when we move the creation of objects out of the consuming classes. You may find it self evident that the external caller (often a DI Container) creates new instances of Dependencies — but when are injected instances deallocated? And what if the external caller doesn’t create new instances each time, but instead hands you an existing instance? These are topics for the next chapter.

概括

Summary

  • 对象组合是建立相关组件层次结构的行为,它发生在Composition Root内部。
  • Object Composition is the act of building up hierarchies of related components, which takes place inside the Composition Root.
  • 组合根应该只做四件事:加载配置值、构建对象图、调用所需的功能以及释放对象图。
  • A Composition Root should only do four things: load configuration values, build object graphs, invoke the desired functionality, and release the object graphs.
  • 只有Composition Root应该依赖配置文件,因为它更灵活,可以由调用者强制配置库。
  • Only the Composition Root should rely on configuration files because it’s more flexible for libraries to be imperatively configurable by their callers.
  • 将配置值的加载与执行对象组合的方法分开。这使得在不存在配置文件的情况下测试对象组合成为可能。
  • Separate the loading of configuration values from the methods that do Object Composition. This makes it possible to test Object Composition without the existence of a configuration file.
  • Model View ViewModel (MVVM) 是一种设计,其中 ViewModel 是视图和模型之间的桥梁。每个 ViewModel 都是一个以特定技术方式转换和公开模型的类。在 MVVM 中,ViewModel 是将使用 DI 组合的应用程序组件。
  • Model View ViewModel (MVVM) is a design in which the ViewModel is the bridge between the view and the model. Each ViewModel is a class that translates and exposes the model in a technology-specific way. In MVVM, ViewModels are the application components that will be composed using DI.
  • 在控制台应用程序中,Program该类是合适的Composition Root
  • In a console application, the Program class is a suitable Composition Root.
  • 在 UWP 应用程序中,App该类是一个合适的Composition Root,其OnLaunched方法是主要入口点。
  • In a UWP application, the App class is a suitable Composition Root, and its OnLaunched method is the main entry point.
  • 在 ASP.NET Core MVC 应用程序中,IControllerActivator是插入Object Composition的正确扩展点。
  • In an ASP.NET Core MVC application, the IControllerActivator is the correct extensibility point to plug in Object Composition.
  • 确保在 ASP.NET Core 中释放依赖HttpContext.Response.RegisterForDispose项的一种实用方法是使用该方法将它们添加到可释放请求对象列表中。
  • A practical way to ensure that Dependencies are disposed of in ASP.NET Core is to use the HttpContext.Response.RegisterForDispose method to add them to the list of disposable request objects.
  • 通过将函数注册到实现Composition Root一小部分的管道,可以将中间件添加到 ASP.NET Core 。这组成了中间件组件并调用它。
  • Middleware can be added to ASP.NET Core by registering a function to the pipeline that implements a small part of the Composition Root. This composes the middleware component and invokes it.

8

对象生命周期

8

Object lifetime

在这一章当中

In this chapter

  • 管理依赖生命周期
  • Managing Dependency Lifetime
  • 使用一次性依赖项
  • Working with disposable Dependencies
  • 使用SingletonTransientScoped 生活方式
  • Using Singleton, Transient, and Scoped Lifestyles
  • 预防或纠正不良的生活方式选择
  • Preventing or fixing bad Lifestyle choices

时间的流逝对大多数食物和饮料都有深远的影响,但后果各不相同。就个人而言,我们发现 12 个月大的 Gruyère 比 6 个月大的 Gruyère 更有趣,但 Mark 更喜欢他的芦笋比这两种都新鲜。1  在许多情况下,很容易评估一件物品的适当年龄;但在某些情况下,这样做会变得很复杂。当涉及到葡萄酒时,这一点最为显着(见图 8.1)。

The passing of time has a profound effect on most food and drink, but the consequences vary. Personally, we find 12-month-old Gruyère more interesting than 6-month-old Gruyère, but Mark prefers his asparagus fresher than either of those.1  In many cases, it’s easy to assess the proper age of an item; but in certain cases, doing so becomes complex. This is most notable when it comes to wine (see figure 8.1).

葡萄酒往往会随着年龄的增长而变得更好——直到它们突然变得太陈旧并失去大部分风味。这取决于许多因素,包括葡萄酒的产地和年份。尽管我们对葡萄酒感兴趣,但我们从不指望我们能够预测葡萄酒何时达到顶峰。为此,我们依靠专家:家里的书籍和餐厅的侍酒师。他们比我们更了解葡萄酒,所以我们很高兴让他们掌控一切。

Wines tend to get better with age — until they suddenly become too old and lose most of their flavor. This depends on many factors, including the origin and vintage of the wine. Although wines interest us, we don’t ever expect we’ll be able to predict when a wine will peak. For that, we rely on experts: books at home and sommeliers at restaurants. They understand wines better than we do, so we happily let them take control.

08-01.tif

图 8.1 葡萄酒、奶酪和芦笋。虽然组合可能有点偏差,但他们的年龄对他们的综合素质影响很大。

Figure 8.1 Wine, cheese, and asparagus. Although the combination may be a bit off, their age greatly affects their overall qualities.

除非您不阅读任何前面的内容而直接进入本章,否则您就会知道放开控制是 DI 中的一个关键概念。这源于控制反转原则,您将对依赖项的控制委托给第三方,但它也意味着不仅仅是让其他人选择所需抽象的实现。当您允许Composer提供Dependency时,您还必须接受您无法控制它的生命周期。

Unless you dove straight into this chapter without reading any of the previous ones, you know that letting go of control is a key concept in DI. This stems from the Inversion of Control principle, where you delegate control of your Dependencies to a third party, but it also implies more than just letting someone else pick an implementation of a required Abstraction. When you allow a Composer to supply a Dependency, you must also accept that you can’t control its lifetime.

正如侍酒师非常了解餐厅酒窖的内容并且可以做出比我们更明智的决定,我们应该相信Composer能够比消费者更有效地控制Dependencies的生命周期。组合和管理组件是它的唯一责任。

Just as the sommelier intimately knows the contents of the restaurant’s wine cellar and can make a far more informed decision than we can, we should trust the Composer to be able to control the lifetime of Dependencies more efficiently than the consumer. Composing and managing components is its single responsibility.

在本章中,我们将探索依赖生命周期管理。理解这个主题很重要,因为如果你在错误的年龄(你自己的年龄和葡萄酒的年龄)喝了葡萄酒,你可能会体验到低于标准的体验,你可能会因为错误地配置Dependency Lifetime而体验到性能下降。更糟糕的是,您可能会得到相当于变质食物的生命周期管理:资源泄漏。理解正确管理组件生命周期的原则应该能够让您做出明智的决定来正确配置您的应用程序。

In this chapter, we’ll explore Dependency Lifetime Management. Understanding this topic is important because, just as you can have a subpar experience if you drink a wine at the wrong age (both your own age and the wine’s), you can experience degraded performance from configuring Dependency Lifetime incorrectly. Even worse, you may get the Lifetime Management equivalent of spoiled food: resource leaks. Understanding the principles of correctly managing the lifecycles of components should enable you to make informed decisions to configure your applications correctly.

我们将从对依赖生命周期管理的一般介绍开始,然后讨论一次性依赖。本章的第一部分旨在提供您需要的所有背景信息和指导原则,以便您对自己的应用程序的生命周期、范围和配置做出明智的决策。

We’ll start with a general introduction to Dependency Lifetime Management, followed by a discussion about disposable Dependencies. This first part of the chapter is meant to provide all the background information and guiding principles you need in order to make knowledgeable decisions about your own applications' lifecycles, scope, and configurations.

之后,我们将研究不同的生命周期策略。本章的这一部分采用可用生活方式目录的形式。在大多数情况下,这些常见的生活方式模式中的一种可以很好地应对给定的挑战,因此提前了解它们可以让您有能力应对许多困难的情况。

After that, we’ll look at different lifetime strategies. This part of the chapter takes the form of a catalog of available Lifestyles. In most cases, one of these stock Lifestyle patterns will provide a good match for a given challenge, so understanding them in advance equips you to deal with many difficult situations.

我们将以一些关于生命周期管理的坏习惯或反模式来结束本章。当我们完成后,您应该很好地掌握了终身管理和常见生活方式的注意事项。首先,让我们看看对象生命周期以及它与一般 DI 的关系。

We’ll finish the chapter with some bad habits, or anti-patterns, concerning Lifetime Management. When we’re finished, you should have a good grasp of Lifetime Management and common Lifestyle do’s and don’ts. First, let’s look at Object Lifetime and how it relates to DI in general.

8.1 管理依赖生命周期

8.1 Managing Dependency Lifetime

到目前为止,我们主要讨论了 DI 如何使您能够组合Dependencies。前一章非常详细地探讨了这个主题,但是,正如我们在 1.4 节中提到的,对象组合只是 DI 的一个方面。管理对象生命周期是另一回事。

Up to this point, we’ve mostly discussed how DI enables you to compose Dependencies. The previous chapter explored this subject in great detail, but, as we alluded to in section 1.4, Object Composition is just one aspect of DI. Managing Object Lifetime is another.

我们第一次了解到 DI 的范围包括Lifetime Management的想法时,我们未能理解Object CompositionObject Lifetime之间的深层联系。终于搞定了,而且很简单,一起来看看吧!

The first time we were introduced to the idea that the scope of DI includes Lifetime Management, we failed to understand the deep connection between Object Composition and Object Lifetime. We finally got it, and it’s simple, so let’s take a look!

在本节中,我们将介绍Lifetime Management以及它如何应用于Dependencies。我们将研究组合对象的一般情况,以及它如何影响Dependencies的生命周期。首先,我们将研究为什么对象组合意味着生命周期管理

In this section, we’ll introduce Lifetime Management and how it applies to Dependencies. We’ll look at the general case of composing objects and how it has implications for the lifetimes of Dependencies. First, we’ll investigate why Object Composition implies Lifetime Management.

8.1.1 引入生命周期管理

8.1.1 Introducing Lifetime Management

当我们接受我们应该放弃对依赖项的控制的心理需求,而是通过构造函数注入或其他 DI 模式之一请求它们时,我们必须完全放手。要了解原因,我们将检查问题逐步解决。让我们首先回顾一下标准 .NET 对象生命周期对Dependencies的意义。您可能已经知道这一点,但在我们建立上下文时请耐心等待下半页。

When we accept that we should let go of our psychological need for control over Dependencies and instead request them through Constructor Injection or one of the other DI patterns, we must let go completely. To understand why, we’ll examine the issue progressively. Let’s begin by reviewing what the standard .NET object lifecycle means for Dependencies. You likely already know this, but bear with us for the next half page while we establish the context.

简单的依赖生命周期

Simple Dependency lifecycle

您知道 DI 意味着您让第三方(通常是我们的Composition Root)提供您需要的依赖项。这也意味着您必须让它管理Dependencies的生命周期。当涉及到对象创建时,这是最容易理解的。这是来自示例电子商务应用程序的Composition Root的(稍微重组的)代码片段。(你可以在清单 7.8 中看到完整的例子。)

You know that DI means you let a third party (typically our Composition Root) serve the Dependencies you need. This also means you must let it manage the Dependencies’ lifetimes. This is easiest to understand when it comes to object creation. Here’s a (slightly restructured) code fragment from the sample e-commerce application’s Composition Root. (You can see the complete example in listing 7.8.)

var productRepository =
    new SqlProductRepository(
        new CommerceContext(connectionString));

var productService =
    new ProductService(
        productRepository,
        userContext);

我们希望ProductService类很明显不控制何时创建。在这种情况下,很可能在同一毫秒内创建;但作为一个思想实验,我们可以在这两行代码之间插入对的调用,以证明您可以随着时间的推移任意地将它们分开。这将是一件非常奇怪的事情,但关键是并非必须同时创建依赖图的所有对象。productRepositorySqlProductRepositoryThread.Sleep

We hope that it’s evident that the ProductService class doesn’t control when productRepository is created. In this case, SqlProductRepository is likely to be created within the same millisecond; but as a thought experiment, we could insert a call to Thread.Sleep between these two lines of code to demonstrate that you can arbitrarily separate them over time. That would be a pretty weird thing to do, but the point is that not all objects of a Dependency graph have to be created at the same time.

消费者不控制其Dependencies的创建,但是销毁呢?作为一般规则,您无法控制对象在 .NET 中何时被销毁。垃圾收集器清理未使用的对象,但除非您处理的是一次性对象,否则您不能显式销毁对象。

Consumers don’t control creation of their Dependencies, but what about destruction? As a general rule, you don’t control when objects are destroyed in .NET. The garbage collector cleans up unused objects, but unless you’re dealing with disposable objects, you can’t explicitly destroy an object.

当对象超出范围时,它们有资格进行垃圾收集。相反,只要其他人持有对它们的引用,它们就会持续存在。尽管消费者不能显式销毁对象——这取决于垃圾收集器——但它可以通过持有引用来使对象保持活动状态。这就是你在使用Constructor Injection时所做的,因为你将Dependency保存在一个私有字段中:

Objects are eligible for garbage collection when they go out of scope. Conversely, they last as long as someone else holds a reference to them. Although a consumer can’t explicitly destroy an object — that’s up to the garbage collector — it can keep the object alive by holding on to the reference. This is what you do when you use Constructor Injection, because you save the Dependency in a private field:

public class HomeController
{
    private readonly IProductService service;

    public HomeController(IProductService service)    ①  
    {
        this.service = service;    ②  
    }
}

这意味着当消费者超出范围时,依赖项也会超出范围。然而,即使消费者超出范围,如果其他对象持有对它的引用,则依赖关系可以继续存在。否则,它将被垃圾收集。因为您是一位经验丰富的 .NET 开发人员,所以这对您来说可能是个老新闻,但现在讨论应该开始变得更有趣了。

This means that when the consumer goes out of scope, so can the Dependency. Even when a consumer goes out of scope, however, the Dependency can live on if other objects hold a reference to it. Otherwise, it’ll be garbage collected. Because you’re an experienced .NET developer, this is probably old news to you, but now the discussion should begin to get more interesting.

增加依赖生命周期的复杂性

Adding complexity to the Dependency lifecycle

到目前为止,我们对依赖生命周期的分析一直很普通,但现在我们可以增加一些复杂性。当多个消费者需要相同的Dependency时会发生什么?一种选择是为每个消费者提供他们自己的实例,如图 8.2所示。

Until now our analysis of the Dependency lifecycle has been mundane, but now we can add some complexity. What happens when more than one consumer requires the same Dependency? One option is to supply each consumer their own instance, as shown in figure 8.2.

08-02.eps

图 8.2 组合多个独立的依赖实例

Figure 8.2 Composing multiple, unique instances of a Dependency

下面的清单由多个消费者组成,它们具有相同Dependency的多个实例,如图 8.2所示。

The following listing composes multiple consumers with multiple instances of the same Dependency, shown in figure 8.2.

清单 8.1 与同一依赖项的多个实例组合

Listing 8.1 Composing with multiple instances of the same Dependency

var repository1 = new SqlProductRepository(connString);  ①  
var repository2 = new SqlProductRepository(connString);  ①  


var productService = new ProductService(repository1);    ②  


var calculator = new DiscountCalculator(repository2);    ③  

当谈到清单 8.1中每个 Repository 的生命周期时,与前面讨论的示例电子商务应用程序的Composition Root相比,没有任何变化。每个依赖项超出范围,并在其使用者超出范围时被垃圾收集。这可能会在不同的时间发生,但情况与以前仅略有不同。如果两个消费者共享相同的依赖关系,情况就会有所不同,如图 8.3所示。

When it comes to the lifecycles of each Repository in listing 8.1, nothing has changed compared to the previously discussed sample e-commerce application’s Composition Root. Each Dependency goes out of scope and is garbage-collected when its consumers go out of scope. This can happen at different times, but the situation is only marginally different than before. It would be a somewhat different situation if both consumers were to share the same Dependency, as shown in figure 8.3.

08-03.eps

图 8.3通过将依赖项注入多个消费者来重用相同的依赖 项实例

Figure 8.3 Reusing the same instance of a Dependency by injecting it into multiple consumers

当你将它应用到清单 8.1时,你会得到清单 8.2中的代码。

When you apply this to listing 8.1, you get the code in listing 8.2.

清单 8.2与相同依赖 项的单个实例组合

Listing 8.2 Composing with a single instance of the same Dependency

var repository = new SqlProductRepository(connString);

var productService = new ProductService(repository);    ①  

var calculator = new DiscountCalculator(repository);    ①  

比较清单 8.1 和 8.2 时,您不会发现一个天生就比另一个更好。正如我们将在 8.3 节中讨论的那样,当涉及到何时以及如何重用Dependency时,有几个因素需要考虑。

When comparing listings 8.1 and 8.2, you don’t find that one is inherently better than the other. As we’ll discuss in section 8.3, there are several factors to consider when it comes to when and how you want to reuse a Dependency.

与前面的示例相比,存储库依赖项的生命周期发生了明显变化。两个消费者都必须在变量有资格进行垃圾收集之前离开范围,并且他们可以在不同的时间这样做。当依赖项到达其生命周期结束时,情况变得更不可预测。只有当消费者数量增加时,这种特性才会得到加强。repository

The lifecycle for the Repository Dependency has changed distinctly, compared with the previous example. Both consumers must go out of scope before the variable repository can be eligible for garbage collection, and they can do so at different times. The situation becomes less predictable when the Dependency reaches the end of its lifetime. This trait is only reinforced when the number of consumers increases.

如果有足够的消费者,很可能总会有一个人来保持依赖性。这听起来像是个问题,但很少是这样:您只有一个,而不是大量相似的实例,这样可以节省内存。这是一种令人向往的品质,我们将其形式化为一种称为单身生活方式的生活方式模式. 不要将其与 Singleton 设计模式混淆,尽管它们有相似之处。2  我们将在第 8.3.1 节中更详细地讨论这个主题。

Given enough consumers, it’s likely that there’ll always be one around to keep the Dependency alive. This may sound like a problem, but it rarely is: instead of a multitude of similar instances, you have only one, which saves memory. This is such a desirable quality that we formalize it in a Lifestyle pattern called the Singleton Lifestyle. Don’t confuse this with the Singleton design pattern, although there are similarities.2  We’ll go into greater detail about this subject in section 8.3.1.

需要注意的关键点是ComposerDependencies生命周期的影响比任何单个消费者都大。Composer决定何时创建实例,并通过选择是否共享实例来确定依赖项是否超出单个消费者的范围,或者是否所有消费者都必须超出范围才能释放依赖项。

The key point to appreciate is that the Composer has a greater degree of influence over the lifetime of Dependencies than any single consumer. The Composer decides when instances are created, and by its choice of whether to share instances, it determines whether a Dependency goes out of scope with a single consumer, or whether all consumers must go out of scope before the Dependency can be released.

这相当于去一家拥有优秀侍酒师的餐厅。侍酒师一天中的大部分时间都花在管理和改进酒窖上:购买新酒,对可用的酒瓶进行取样以跟踪它们的发展情况,并与厨师合作确定与所供应食物的最佳搭配。当我们看到酒单时,它只包括侍酒师认为适合今天菜单的酒。我们可以根据个人口味自由选择葡萄酒,但我们并不认为比侍酒师更了解餐厅的葡萄酒选择以及它们与食物的搭配。侍酒师通常会决定将大量酒瓶库存多年;正如您将在下一节中看到的那样,一个Composer可能会决定通过保留它们的引用来使实例保持活动状态。

This is comparable to visiting a restaurant with a good sommelier. The sommelier spends a large proportion of the day managing and evolving the wine cellar: buying new wines, sampling the available bottles to track how they develop, and working with the chefs to identify optimal matches to the food being served. When we’re presented with the wine list, it includes only what the sommelier deems fit to offer for today’s menu. We’re free to select a wine according to our personal taste, but we don’t presume to know more about the restaurant’s selection of wines and how they go with the food than the sommelier does. The sommelier will often decide to keep lots of bottles in stock for years; and as you’ll see in the next section, a Composer may decide to keep instances alive by holding on to their references.

8.1.2 使用Pure DI管理生命周期

8.1.2 Managing lifetime with Pure DI

上一节解释了如何改变依赖关系的组成来影响它们的生命周期。在本节中,我们将了解如何使用Pure DI实现这一点,同时应用两种最常用的LifestylesTransientSingleton

The previous section explained how you can vary the composition of Dependencies to influence their lifetimes. In this section, we’ll look at how to implement this using Pure DI, while applying the two most commonly used Lifestyles: Transient and Singleton.

在第 7 章中,您创建了专门的类来组合应用程序。其中之一是用于 ASP.NET Core MVC 应用程序——我们的Composer。清单 7.8 显示了其方法的实现。CommerceControllerActivatorCreate

In chapter 7, you created specialized classes to compose applications. One of these was a CommerceControllerActivator for an ASP.NET Core MVC application — our Composer. Listing 7.8 shows the implementation of its Create method.

你可能还记得,Create方法每次调用时都会即时创建整个对象图。每个依赖项对于发布的控制器都是私有的,并且没有共享。当控制器实例超出范围时(每次服务器响应请求时它都会这样做),所有依赖项也会超出范围。这通常被称为瞬态生活方式,我们将在 8.3.2 节中详细讨论。

As you may recall, the Create method creates the entire object graph on the fly each time it’s invoked. Each Dependency is private to the issued controller, and there’s no sharing. When the controller instance goes out of scope (which it does every time the server has replied to a request), all the Dependencies go out of scope too. This is often called a Transient Lifestyle, which we’ll talk more about in section 8.3.2.

让我们分析图 8.4中所示的 和所创建的对象图,看看是否有改进的余地。和类都是完全无状态的服务,因此没有理由在每次需要为请求提供服务时都创建一个新实例。连接字符串也不太可能更改,因此您可以跨请求重用它。班级_CommerceControllerActivatorAspNetUserContextAdapterRouteCalculatorSqlProductRepository另一方面,它依赖于一个实体框架DbContext(由我们的 实现CommerceContext),它不能在请求之间共享。3个 

Let’s analyze the object graphs created by the CommerceControllerActivator and shown in figure 8.4 to see if there’s room for improvement. Both the AspNetUserContextAdapter and RouteCalculator classes are completely stateless services, so there’s no reason to create a new instance every time you need to service a request. The connection string is also unlikely to change, so you can reuse it across requests. The SqlProductRepository class, on the other hand, relies on an Entity Framework DbContext (implemented by our CommerceContext), which mustn’t be shared across requests.3 

08-04.eps

图 8.4 创建的对象图CommerceControllerActivator,它创建HomeControllerRouteController实例及其依赖关系

Figure 8.4 Object graphs as created by CommerceControllerActivator, which creates HomeController and RouteController instances with their Dependencies

鉴于此特定配置,更好的实现将重用 和 的相同实例,同时创建 和的新实例。简而言之,您应该配置并使用Singleton Lifestyle,并将and设置为Transient。以下清单显示了如何实现此更改。CommerceControllerActivatorAspNetUserContextAdapterRouteCalculatorProductServiceSqlProductRepositoryAspNetUserContextAdapterRouteCalculatorProductServiceSqlProductRepository

Given this particular configuration, a better implementation of CommerceControllerActivator would reuse the same instances of both AspNetUserContextAdapter and RouteCalculator, while creating new instances of ProductService and SqlProductRepository. In short, you should configure AspNetUserContextAdapter and RouteCalculator to use the Singleton Lifestyle, and ProductService and SqlProductRepository as Transient. The following listing shows how to implement this change.

清单 8.3CommerceControllerActivator

Listing 8.3 Managing lifetime within the CommerceControllerActivator

public class CommerceControllerActivator : IControllerActivator
{
    private readonly string connectionString;
    private readonly IUserContext userContext;    ①  
    private readonly RouteCalculator calculator;    ①  

    public CommerceControllerActivator(string connectionString)
    {
        this.connectionString = connectionString;

        this.userContext =    ②  
            new AspNetUserContextAdapter();    ②  
    ②  
        this.calculator =    ②  
            new RouteCalculator(    ②  
                this.CreateRouteAlgorithms());    ②  
    }

    public object Create(ControllerContext context)
    {
        Type type = context.ActionDescriptor
            .ControllerTypeInfo.AsType();

        switch (type.Name)
        {
            case "HomeController":
                return this.CreateHomeController();

            case "RouteController":
                return this.CreateRouteController();

            default:
                throw new Exception("Unknown controller " + type.Name);

        }
    }

    private HomeController CreateHomeController()
    {
        return new HomeController(    ③  
            new ProductService(    ③  
                new SqlProductRepository(    ③  
                    new CommerceContext(  ③  
                        this.connectionString)),  ③  
                this.userContext));    ③  
    }    ③  
    ③  
    private RouteController CreateRouteController()    ③  
    {    ③  
        return new RouteController(this.calculator);    ③  
    }

    public void Release(ControllerContext context,    ④  
        object controller) { ... }    ④  
}

Startup在 MVC 应用程序中,在类中加载配置值很实用. 这就是为什么在清单 8.3中,连接字符串被提供给.CommerceControllerActivator

In an MVC application, it’s practical to load configuration values in the Startup class. That’s why in listing 8.3, the connection string is supplied to the constructor of the CommerceControllerActivator.

清单 8.3中的代码在功能上等同于清单 7.8 中的代码——只是效率稍微高一点,因为一些依赖项是共享的。通过保留您创建的依赖项,您可以让它们一直存在。在此示例中,初始化后立即CommerceControllerActivator创建了两个Singleton Dependencies,但它也可以使用惰性初始化。

The code in listing 8.3 is functionally equivalent to the code in listing 7.8 — it’s just slightly more efficient because some of the Dependencies are shared. By holding on to the Dependencies you create, you can keep them alive for as long as you want. In this example, CommerceControllerActivator created both Singleton Dependencies as soon as it was initialized, but it could also have used lazy initialization.

微调每个DependencyLifestyle的能力对于性能原因可能很重要,但对于正确的行为也很重要。例如,Mediator 设计模式依赖于一个共享的导向器,多个组件通过它进行通信。4  这仅在相关协作者共享调解器时有效。

The ability to fine-tune each Dependency’s Lifestyle can be important for performance reasons, but can also be important for correct behavior. For instance, the Mediator design pattern relies on a shared director through which several components communicate.4  This only works when the Mediator is shared among the involved collaborators.

到目前为止,我们已经讨论了控制反转如何暗示消费者无法管理其Dependencies的生命周期,因为他们不控制对象的创建;并且由于 .NET 使用垃圾收集,消费者也不能显式销毁对象。这就留下了一个悬而未决的问题:disposable Dependencies怎么样?我们现在将注意力转向这个微妙的问题。

So far, we’ve discussed how Inversion of Control implies that consumers can’t manage the lifetimes of their Dependencies, because they don’t control creation of objects; and because .NET uses garbage collection, consumers can’t explicitly destroy objects, either. This leaves a question unanswered: what about disposable Dependencies? We’ll now turn our attention to that delicate question.

8.2 使用一次性依赖

8.2 Working with disposable Dependencies

尽管 .NET 是一个带有垃圾收集器的托管平台,它仍然可以与非托管代码交互。发生这种情况时,.NET 代码会与未进行垃圾回收的非托管内存交互。为防止内存泄漏,您必须有一种机制来确定性地释放非托管内存。这是IDisposable接口的主要目的.

Although .NET is a managed platform with a garbage collector, it can still interact with unmanaged code. When this happens, .NET code interacts with unmanaged memory that isn’t garbage-collected. To prevent memory leaks, you must have a mechanism with which to deterministically release unmanaged memory. This is the key purpose of the IDisposable interface.

某些依赖项实现可能包含非托管资源。例如,ADO.NET 连接是一次性的,因为它们倾向于使用非托管内存。因此,与数据库相关的实现(如由数据库支持的存储库)本身很可能是一次性的。我们应该如何对一次性依赖建模?我们是否也应该让抽象成为一次性的?这可能看起来像这样:

It’s likely that some Dependency implementations will contain unmanaged resources. As an example, ADO.NET connections are disposable because they tend to use unmanaged memory. As a result, database-related implementations like Repositories backed by databases are likely to be disposable themselves. How should we model disposable Dependencies? Should we also let Abstractions be disposable? That might look like this:

气味.tif
public interface IMyDependency : IDisposable

如果您有向IDisposable界面添加内容的冲动,那可能是因为您心中有一个特定的实现。但是你一定不能让这些知识渗透到界面设计中。这样做会使其他类更难以实现该接口,并会在Abstraction中引入模糊性。

If you feel the urge to add IDisposable to your interface, it’s probably because you have a particular implementation in mind. But you must not let that knowledge leak through to the interface design. Doing so would make it more difficult for other classes to implement the interface and would introduce vagueness into the Abstraction.

谁负责处理一次性依赖项?会不会是消费者?

Who’s responsible for disposing of a disposable Dependency? Could it be the consumer?

8.2.1 消费一次性依赖

8.2.1 Consuming disposable Dependencies

为了争论,假设你有一个一次性的抽象,如下面的IOrderRepository接口.

For the sake of argument, imagine that you have a disposable Abstraction like the following IOrderRepository interface.

气味.tif

清单 8.4 IOrderRepository实现IDisposable

Listing 8.4 IOrderRepository implementing IDisposable

public interface IOrderRepository : IDisposable

OrderService一堂课应该怎样处理这样的依赖?大多数设计指南(包括 Visual Studio 的内置代码分析)都坚持认为,如果一个类作为成员持有一次性资源,它应该自己实现IDisposable和处置该资源。下一个清单显示了如何。

How should an OrderService class deal with such a Dependency? Most design guidelines (including Visual Studio’s built-in Code Analysis) would insist that if a class holds a disposable resource as a member, it should itself implement IDisposable and dispose of the resource. The next listing shows how.

坏.tif

清单 8.5 OrderService依赖一次性依赖

Listing 8.5 OrderService depending on disposable Dependency

public sealed class OrderService : IDisposable    ①  
{
    private readonly IOrderRepository repository;

    public OrderService(IOrderRepository repository)
    {
        this.repository = repository;
    }

    public void Dispose()
    {
        this.repository.Dispose();    ②  
    }
}

但事实证明这是一个坏主意,因为repository成员最初是注入的,它可以被其他消费者共享:

But this turns out to be a bad idea because the repository member was originally injected, and it can be shared by other consumers:

var repository =
    new SqlOrderRepository(connectionString);

var validator = new OrderValidator(repository);    ①  
var orderService = new OrderService(repository);    ①  

orderService.AcceptOrder(order);
orderService.Dispose();    ②  

validator.Validate(order);    ③  

不处理注入的存储库会减少危险,但这意味着您忽略了抽象是一次性的事实。此外,在这种情况下,抽象公开的成员多于客户端使用的成员,这违反了接口隔离原则(参见第 6.2.1 节)。将抽象声明为派生自IDisposable没有任何好处。

It would be less dangerous not to dispose of the injected Repository, but this means you’re ignoring the fact that the Abstraction is disposable. Besides, in this case, the Abstraction exposes more members than used by the client, which is an Interface Segregation Principle violation (see section 6.2.1). Declaring an Abstraction as deriving from IDisposable provides no benefit.

话又说回来,在某些情况下,您需要发出短期作用域开始和结束的信号;IDisposable有时用于此目的。在我们研究Composer如何管理一次性Dependency的生命周期之前,我们应该考虑如何处理这种短暂的一次性用品。

Then again, there can be scenarios where you need to signal the beginning and end of a short-lived scope; IDisposable is sometimes used for that purpose. Before we examine how a Composer can manage the lifetime of a disposable Dependency, we should consider how to deal with such ephemeral disposables.

创建临时一次性用品

Creating ephemeral disposables

.NET BCL 中的许多 API 用于IDisposable发出特定范围已结束的信号。一个更突出的例子是 WCF 代理。

Many APIs in the .NET BCL use IDisposable to signal that a particular scope has ended. One of the more prominent examples is WCF proxies.

重要的是要记住,IDisposable出于此类目的使用 并不一定表示存在漏洞抽象,因为这些类型并不总是首先是抽象。另一方面,其中一些是;在这种情况下,您如何处理它们?

It’s important to remember that the use of IDisposable for such purposes need not indicate a Leaky Abstraction, because these types aren’t always Abstractions in the first place. On the other hand, some of them are; and when that’s the case, how do you deal with them?

幸运的是,一个对象被销毁后,你就不能再使用它了。如果你想再次调用相同的 API,你必须创建一个新的实例。作为一个非常适合您如何使用 WCF 代理或 ADO.NET 命令的示例,您创建代理,调用它的操作,并在完成后立即处理它。如果您认为一次性抽象是有漏洞的抽象,您如何将其与 DI 协调起来?

Fortunately, after an object is disposed of, you can’t reuse it. If you want to invoke the same API again, you must create a new instance. As an example that fits well with how you use WCF proxies or ADO.NET commands, you create the proxy, invoke its operations, and dispose of it as soon as you’re finished. How can you reconcile this with DI if you consider disposable Abstractions to be Leaky Abstractions?

与往常一样,将混乱的细节隐藏在界面后面可能会有所帮助。回到第 7.2 节的 UWP 应用程序,我们使用IProductRepository 抽象从表示逻辑层隐藏与数据存储通信的细节。在此讨论期间,我们忽略了此类实现的细节,因为它在当时并不那么重要。但我们假设 UWP 应用程序必须与 WCF Web 服务通信。来自EditProductViewModel透视,这是删除产品的方式:

As always, hiding the messy details behind an interface can be helpful. Returning to the UWP application from section 7.2, we used an IProductRepository Abstraction to hide the details of communicating with a data store from the presentation logic layer. During this discussion, we ignored the details of such an implementation because it wasn’t that relevant at that moment. But let’s assume that the UWP application must communicate with a WCF web service. From the EditProductViewModel’s perspective, this is how you delete a product:

private void DeleteProduct()
{
    this.productRepository.Delete(this.Model.Id);    ①  
    this.whenDone();
}

当我们查看该接口的 WCF 实现时,会形成另一幅图。下面是WcfProductRepositoryDelete方法的实现。

Another picture forms when we look at the WCF implementation of that interface. Here’s the implementation of WcfProductRepository with its Delete method.

清单 8.6 使用 WCF 通道作为临时一次性

Listing 8.6 Using a WCF channel as an ephemeral disposable

public class WcfProductRepository : IProductRepository
{
    private readonly ChannelFactory<IProductManagementService> factory;

    public WcfProductRepository(
        ChannelFactory<IProductManagementService> factory)
    {
        this.factory = factory;
    }

    public void Delete(Guid productId)    ①  
    {    ①  
        using (var channel =    ①  
            this.factory.CreateChannel())    ①  
        {    ①  
            channel.DeleteProduct(productId);    ①  
        }    ①  
    }
    ...
}

WcfProductRepository班级_没有可变状态,因此您可以注入一个ChannelFactory<TChannel>可用于创建通道的。通道只是 WCF 代理的另一个词,它是您在使用 Visual Studio 或 svcutil.exe 创建服务引用时免费获得的自动生成的客户端界面。

The WcfProductRepository class has no mutable state, so you inject a ChannelFactory<TChannel> that you can use to create a channel. Channel is just another word for a WCF proxy, and it’s the autogenerated client interface you get for free when you create a service reference with Visual Studio or svcutil.exe.

因为此接口派生自IDisposable,所以您可以将其包装在一条using语句中。然后您使用该渠道删除产品。当您退出using作用域时,通道将被丢弃。

Because this interface derives from IDisposable, you can wrap it in a using statement. You then use the channel to delete the product. When you exit the using scope, the channel is disposed of.

每次调用WcfProductRepository类上的方法时,它会快速打开一个新通道并在使用后对其进行处理。它的生命周期非常短,这就是为什么我们称这种一次性抽象为短暂的一次性。

Every time you invoke a method on the WcfProductRepository class, it quickly opens a new channel and disposes of it after use. Its lifetime is extremely short, which is why we call such a disposable Abstraction an ephemeral disposable.

可是等等!我们不是说一次性抽象是有漏洞的抽象吗?是的,我们做到了,但我们必须在务实考虑与原则之间取得平衡。在这种情况下,至少WcfProductRepository和是在同一个特定于 WCF 的库中定义的。这确保了Leaky Abstraction可以被限制在对了解和管理这种复杂性有合理期望的代码中。IProductManagementService

But wait! Didn’t we claim that a disposable Abstraction is a Leaky Abstraction? Yes, we did, but we have to balance pragmatic concerns against principles. In this case, at least, WcfProductRepository and IProductManagementService are defined in the same WCF-specific library. This ensures that the Leaky Abstraction can be confined to code that has a reasonable expectation of knowing about and managing that complexity.

请注意,短暂的一次性物品永远不会注入消费者。取而代之的是,使用了一个工厂,您可以使用该工厂来控制临时一次性物品的生命周期。

Notice that the ephemeral disposable is never injected into the consumer. Instead, a factory is used, and you use that factory to control the lifetime of the ephemeral disposable.

ChannelFactory<TChannel>是线程安全的,可以作为Singleton注入。在这种情况下,您可能想知道为什么我们选择注入ChannelFactory<TChannel>WcfProductRepository的构造函数中;您可以在内部创建它并将其存储在一个static字段中。然而,这会导致WcfProductRepository隐式依赖配置文件,该文件需要存在才能创建新的WcfProductRepository. 正如我们在 2.2.3 中讨论的那样,只有完成的应用程序才应该依赖配置文件。

ChannelFactory<TChannel> is thread-safe and can be injected as a Singleton. In this case, you might wonder why we choose to inject ChannelFactory<TChannel> into the WcfProductRepository’s constructor; you can create it internally and store it in a static field. This, however, causes WcfProductRepository to be implicitly dependent on a configuration file, which needs to exist to create a new WcfProductRepository. As we discussed in 2.2.3, only the finished application should rely on configuration files.

总之,disposable AbstractionsLeaky Abstractions。有时我们必须接受这样的泄漏以避免错误(例如拒绝 WCF 连接);但是当我们这样做时,我们应该尽最大努力控制泄漏,这样它就不会传播到整个应用程序。我们现在已经研究了如何使用一次性依赖项。让我们将注意力转向如何为消费者服务和管理它们。

In summary, disposable Abstractions are Leaky Abstractions. Sometimes we must accept such a leak to avoid bugs (such as refused WCF connections); but when we do that, we should do our best to contain that leak so it doesn’t propagate throughout an entire application. We’ve now examined how to consume disposable Dependencies. Let’s turn our attention to how we can serve and manage them for consumers.

8.2.2 管理一次性依赖

8.2.2 Managing disposable Dependencies

因为我们如此坚定地坚持一次性抽象泄漏抽象,结果是抽象不应该是一次性的。另一方面,有时实现是一次性的;如果您没有妥善处理它们,您的应用程序就会出现资源泄漏。必须有人或某事处理掉它们。

Because we so adamantly insist that disposable Abstractions are Leaky Abstractions, the consequence is that Abstractions shouldn’t be disposable. On the other hand, sometimes implementations are disposable; if you don’t properly dispose of them, you’ll have resource leaks in your applications. Someone or something must dispose of them.

一如既往,这个责任落在Composer身上。它比其他任何东西都知道它何时创建一次性实例,因此它也知道何时需要处置该实例。Composer很容易保留对一次性实例的引用并Dispose在适当的时间调用它的方法。挑战在于确定何时是合适的时间。您如何知道何时所有消费者都超出了范围?

As always, this responsibility falls on the Composer. It, better than anything else, knows when it creates a disposable instance, so it also knows when the instance needs to be disposed of. It’s easy for the Composer to keep a reference to the disposable instance and invoke its Dispose method at an appropriate time. The challenge lies in identifying when it’s the appropriate time. How do you know when all consumers have gone out of scope?

除非您在发生这种情况时被告知,否则您不知道。然而,您的代码通常存在于某种具有明确定义的生命周期的上下文中,以及告诉您特定范围何时完成的事件。例如,在 ASP.NET Core 中,您可以围绕单个 Web 请求确定实例范围。在 Web 请求结束时,框架会告诉IControllerActivator,通常是我们的Composer,它应该释放给定对象的所有依赖项。然后由Composer来跟踪这些Dependencies并决定是否必须根据他们的Lifestyles处理任何东西。

Unless you’re informed when that happens, you don’t know. Often, however, your code lives inside some sort of context with a well-defined lifetime, as well as events that tell you when a specific scope completes. In ASP.NET Core, for instance, you can scope instances around a single web request. At the end of a web request, the framework tells IControllerActivator, which is typically our Composer, that it should release all Dependencies for a given object. It’s then up to the Composer to keep track of those Dependencies and to decide whether anything must be disposed of based on their Lifestyles.

释放依赖

Releasing Dependencies

释放对象图与处置它不同。正如我们在介绍中所说,释放是确定哪些依赖项可以被取消引用并可能被处理掉,以及哪些依赖项应该保持活动状态以供重用的过程。Composer决定释放的对象是应该被释放还是被重用。

Releasing an object graph isn’t the same as disposing of it. As we stated in the introduction, releasing is the process of determining which Dependencies can be dereferenced and possibly disposed of, and which Dependencies should be kept alive to be reused. It’s the Composer that decides whether a released object should be disposed of or reused.

对象图的释放是向Composer发出的信号,表明图的根超出范围,因此如果根本身实现IDisposable了 ,则应将其丢弃。但是根的依赖关系可以与其他根共享,因此Composer可能会决定保留其中的一些,因为它知道其他对象仍然依赖它们。图 8.5说明了事件的顺序。

The release of an object graph is a signal to the Composer that the root of the graph is going out of scope, so if the root itself implements IDisposable, then it should be disposed of. But the root’s Dependencies can be shared with other roots, so the Composer may decide to keep some of them around, because it knows other objects still rely on them. Figure 8.5 illustrates the sequence of events.

08-05.eps

图 8.5 释放依赖的事件顺序

Figure 8.5 The sequence of events for releasing Dependencies

要释放DependenciesComposer必须跟踪它曾经服务过的所有一次性Dependencies ,以及它为哪些消费者提供服务,以便它可以在最后一个消费者被释放时处理它们。Composer必须注意以正确的顺序处理对象。

To release Dependencies, a Composer must track all the disposable Dependencies it has ever served, and to which consumers it has served them, so that it can dispose of them when the last consumer is released. And a Composer must take care to dispose of objects in the correct order.

让我们回到清单 8.3中的示例。事实证明,该清单中存在一个错误,因为实现了. 清单 8.3中的代码创建了 的新实例,但它从不处理这些实例。这可能会导致资源泄漏,所以让我们用新版本的Composer修复这个错误。CommerceControllerActivatorCommerceContextIDisposableCommerceContext

Let’s go back to the CommerceControllerActivator example from listing 8.3. As it turns out, there’s a bug in that listing, because CommerceContext implements IDisposable. The code in listing 8.3 creates new instances of CommerceContext, but it never disposes of those instances. This could cause resource leaks, so let’s fix that bug with a new version of the Composer.

首先,请记住 Web 应用程序的Composer必须能够为许多并发请求提供服务,因此它必须将每个CommerceContext实例与其创建的根对象或与其关联的请求相关联。在下面的示例中,我们将使用请求来跟踪一次性对象,因为这使我们不必定义静态字典实例。静态可变状态更难正确使用,因为它必须以线程安全的方式实现。下一个清单显示了如何解析实例CommerceControllerActivator请求。HomeController

First, keep in mind that the Composer for a web application must be able to service many concurrent requests, so it has to associate each CommerceContext instance with either the root object it creates or with the request it’s associated with. In the following example, we’ll use the request to track disposable objects, because this saves us from having to define a static dictionary instance. A static mutable state is more difficult to use correctly, because it must be implemented in a thread-safe manner. The next listing shows how CommerceControllerActivator resolves requests for HomeController instances.

清单 8.7 将 一次性依赖项与 Web 请求相关联

Listing 8.7 Associating disposable Dependencies with a web request

private HomeController CreateHomeController(ControllerContext context)
{
    var dbContext =
        new CommerceContext(this.connectionString);    ①  

    TrackDisposable(context, dbContext);    ②  

    return new HomeController(
        new ProductService(
            new SqlProductRepository(dbContext),
            this.userContext));
}

private static void TrackDisposable(
    ControllerContext context, IDisposable disposable)
{    ③  
    IDictionary<object, object> items =    ③  
        context.HttpContext.Items;    ③  
    ③  
    object list;    ③  
    ③  
    if (!items.TryGetValue("Disposables", out list))    ③  
    {    ③  
        list = new List<IDisposable>();    ③  
        items["Disposables"] = list;    ③  
    }    ③  
    ③  
    ((List<IDisposable>)list).Add(disposable);    ③  
}

CreateHomeController方法_首先解决所有Dependencies。这类似于清单 8.3中的实现,但在返回已解析的服务之前,它必须将依赖关系与请求一起存储,以便在控制器释放时可以将其处理掉。清单 8.7的应用程序流程如图 8.6所示。

The CreateHomeController method starts by resolving all the Dependencies. This is similar to the implementation in listing 8.3, but before returning the resolved service, it must store the Dependency with the request in such a way that it can be disposed of when the controller gets released. The application flow of listing 8.7 is shown in figure 8.6.

08-06.eps

图 8.6 跟踪一次性依赖项

Figure 8.6 Tracking disposable Dependencies

当我们实现清单 7.8 中的 时,我们将该方法留空。到目前为止,我们还没有实现这个方法,依赖垃圾收集器来完成这项工作;但是对于一次性依赖项,您必须借此机会进行清理。这是实现。CommerceControllerActivatorRelease

When we implemented the CommerceControllerActivator in listing 7.8, we left the Release method empty. So far, we haven’t implemented this method, relying on the garbage collector to do the job; but with disposable Dependencies, it’s essential that you take this opportunity to clean up. Here’s the implementation.

清单 8.8 释放一次性依赖

Listing 8.8 Releasing disposable Dependencies

public void Release(ControllerContext context, object controller)
{
    var disposables =    ①  
        (List<IDisposable>)context.HttpContext    ①  
            .Items["Disposables"];    ①  

    if (disposables != null)
    {
        disposables.Reverse();    ②  

        foreach (IDisposable disposable in disposables)  ③  
        {    ③  
            disposable.Dispose();    ③  
        }
    }
}

这个Release方法采取了一条捷径,防止在抛出异常时丢弃一些一次性用品。如果你一丝不苟,你需要确保继续处理实例,即使一个抛出异常,最好通过使用和语句tryfinally. 我们将把它作为练习留给读者。

This Release method takes a shortcut that prevents some disposables from being disposed of if an exception is thrown. If you’re meticulous, you’ll need to ensure that disposal of instances continues, even if one throws an exception, preferably by using try and finally statements. We’ll leave this as an exercise for the reader.

在 ASP.NET Core MVC 的上下文中,使用和的给定解决方案可以简化为对 的简单调用,因为那样可以有效地做同样的事情。它既实现了相反顺序的处理,又在发生故障时继续处理对象。因为本章不是专门介绍 ASP.NET Core MVC,所以我们想为您提供一个更通用的解决方案来说明基本思想。TrackDisposableReleaseHttpContext.Response.RegisterForDispose

In the context of ASP.NET Core MVC, the given solution using TrackDisposable and Release can be reduced to a simple call to HttpContext.Response.RegisterForDispose, because that would effectively do the same thing. It both implements opposite-order disposal and continues disposing of objects in case of a failure. Because this chapter isn’t about ASP.NET Core MVC in particular, we wanted to provide you with a more generic solution that illustrates the basic idea.

Dependencies应该放在哪里?

Where should Dependencies be released?

看完这一切,还有两个问题:对象图应该在哪里发布,谁负责做这件事?重要的是要注意请求对象图的代码也负责请求它的释放。因为对对象图的请求通常是Composition Root的一部分,所以它的发布的启动也是如此。

After reading all this, two questions remain: where should object graphs be released, and who is responsible for doing this? It’s important to note that the code that has requested an object graph is also responsible for requesting its release. Because the request for an object graph is typically part of the Composition Root, so is the initiation of its release.

下面的清单Main再次显示了 7.1 节的控制台应用程序的方法,但现在多了一个Release方法。

The following listing shows the Main method of the console application of section 7.1 again, but now with an additional Release method.

清单 8.9释放已解析对象图 的Composition Root

Listing 8.9 The Composition Root that releases the resolved object graph

static void Main(string[] args)
{
    string connStr = LoadConnectionString();

    CurrencyParser parser =
        CreateCurrencyParser(connStr);    ①  

    ICommand command = parser.Parse(args);    ②  
    command.Execute();    ②  

    Release(parser);    ③  
}

在构建控制台应用程序时,您可以完全控制该应用程序。正如我们在 7.1 节中讨论的那样,没有控制反转。如果您使用的是框架,您会经常看到框架控制请求对象图和要求释放它。ASP.NET Core MVC 就是一个很好的例子。在 MVC 的情况下,它是调用和方法CommerceControllerActivator的框架。在这些调用之间,它使用解析的控制器实例。CreateRelease

When building a console application, you’re in full control of the application. As we discussed in section 7.1, there’s no Inversion of Control. If you’re using a framework, you’ll often see the framework take control over both requesting the object graph and demanding its release. ASP.NET Core MVC is a good example of this. In the case of MVC, it’s the framework that calls CommerceControllerActivator’s Create and Release methods. In between those calls, it uses a resolved controller instance.

我们现在已经详细讨论了生命周期管理。作为消费者,您无法管理注入的Dependencies的生命周期;该责任落在Composer身上,他可以决定在许多消费者之间共享一个实例,或者为每个消费者提供自己的私有实例。这些SingletonTransient Lifestyles只是一大组Lifestyles中最常见的成员,我们将在下一节中介绍最常见的生命周期策略目录。

We’ve now discussed Lifetime Management in some detail. As a consumer, you can’t manage the lifetime of injected Dependencies; that responsibility falls on the Composer who can decide to share a single instance among many consumers or give each consumer its own private instance. These Singleton and Transient Lifestyles are only the most common members of a larger set of Lifestyles, and we’ll use the next section to work our way through a catalog of the most common lifecycle strategies.

8.3 生活方式目录

8.3 Lifestyle catalog

现在我们已经介绍了终身管理背后的原则,我们将花一些时间研究常见的生活方式模式。正如我们在介绍中所描述的,Lifestyle是一种描述Dependency预期生命周期的形式化方式。这给了我们一个共同的词汇表,就像设计模式一样。它使推断依赖项何时以及如何超出范围以及是否会被重用变得更容易。

Now that we’ve covered the principles behind Lifetime Management, we’ll spend some time looking at common Lifestyle patterns. As we described in the introduction, a Lifestyle is a formalized way of describing the intended lifetime of a Dependency. This gives us a common vocabulary, just as design patterns do. It makes it easier to reason about when and how a Dependency is expected to go out of scope — and if it’ll be reused.

本节讨论表 8.1中描述的三种最常见的生活方式。因为您已经遇到过SingletonTransient,所以我们将从这些开始。

This section discusses the three most common Lifestyles described in table 8.1. Because you’ve already encountered both Singleton and Transient, we’ll begin with those.

表 8.1 本节涵盖的生活方式模式
姓名描述
单例一个实例被永久重用。
短暂的始终提供新实例。
范围至多,每个类型的一个实例在每个隐式或显式定义的范围内提供服务。

Scoped Lifestyle的使用很普遍;大多数异国情调的生活方式都是它的变体。与高级Lifestyles相比,Singleton Lifestyle可能看起来平淡无奇,但它仍然是一种常见且适当的生命周期策略。

The use of a Scoped Lifestyle is widespread; most exotic Lifestyles are variations of it. Compared to advanced Lifestyles, a Singleton Lifestyle may seem mundane, but it’s nevertheless a common and appropriate lifecycle strategy.

8.3.1 单身人士的生活方式

8.3.1 The Singleton Lifestyle

在本书中,我们时不时地隐含地使用了单例生活方式。这个名字既清晰又有点混乱。然而,这是有道理的,因为由此产生的行为类似于 Singleton 设计模式,但结构不同。

In this book, we’ve implicitly used the Singleton Lifestyle from time to time. The name is both clear and somewhat confusing at the same time. It makes sense, however, because the resulting behavior is similar to the Singleton design pattern, but the structure is different.

对于Singleton Lifestyle和 Singleton 设计模式,只有一个Dependency实例,但相似之处仅此而已。Singleton 设计模式提供了对其实例的全局访问点,这类似于我们在 5.3 节中讨论的Ambient Context反模式。但是,消费者无法通过静态成员访问Singleton范围的依赖项。如果您要求两个不同的Composer为一个实例提供服务,您将得到两个不同的实例。因此,重要的是不要将单例生活方式与单例设计模式混淆。

With both the Singleton Lifestyle and the Singleton design pattern, there’s only one instance of a Dependency, but the similarity ends there. The Singleton design pattern provides a global point of access to its instance, which is similar to the Ambient Context anti-pattern we discussed in section 5.3. A consumer, however, can’t access a Singleton-scoped Dependency through a static member. If you ask two different Composers to serve an instance, you’ll get two different instances. It’s important, therefore, that you don’t confuse the Singleton Lifestyle with the Singleton design pattern.

因为只有一个实例在使用,单例生活方式通常消耗最少的内存并且是高效的。唯一不是这种情况的情况是实例很少使用但消耗大量内存。在这种情况下,实例可以包装在虚拟代理中,我们将在 8.4.2 节中讨论。

Because only a single instance is in use, the Singleton Lifestyle generally consumes a minimal amount of memory and is efficient. The only time this isn’t the case is when the instance is used rarely but consumes large amounts of memory. In such cases, the instance can be wrapped in a Virtual Proxy, as we’ll discuss in section 8.4.2.

何时使用单身生活方式

When to use the Singleton Lifestyle

尽可能使用单身生活方式。可能会阻止您使用Singleton的两个主要问题如下:

Use the Singleton Lifestyle whenever possible. Two main issues that might prevent you from using a Singleton follow:

  • 当一个组件不是线程安全的。因为Singleton实例可能在许多消费者之间共享,所以它必须能够处理并发访问。
  • When a component isn’t thread-safe. Because the Singleton instance is potentially shared among many consumers, it must be able to handle concurrent access.
  • 当组件的依赖项之一的生命周期预期更短时,可能是因为它不是线程安全的。给组件一个Singleton Lifestyle会使它的Dependencies存活太久。在那种情况下,这样的依赖关系就变成了俘虏依赖关系。我们将在 8.4.1 节中详细介绍Captive Dependencies 。
  • When one of the component’s Dependencies has a lifetime that’s expected to be shorter, possibly because it isn’t thread-safe. Giving the component a Singleton Lifestyle would keep its Dependencies alive for too long. In that case, such a Dependency becomes a Captive Dependency. We’ll go into more detail about Captive Dependencies in section 8.4.1.

根据定义,所有无状态服务都是线程安全的,不可变类型也是如此,显然,专门设计为线程安全的类也是如此。在这些情况下,没有理由不将它们配置为Singletons

All stateless services are, by definition, thread-safe, as are immutable types and, obviously, classes specifically designed to be thread-safe. In these cases, there’s no reason not to configure them as Singletons.

除了效率方面的争论之外,某些依赖关系只有在共享时才能按预期工作。例如,我们将在第 9 章中讨论的 Circuit Breaker 7  设计模式以及内存缓存的实现就是这种情况。在这些情况下,实现是线程安全的至关重要。

In addition to the argument for efficiency, some Dependencies may work as intended only if they’re shared. For example, this is the case for implementations of the Circuit Breaker7  design pattern that we’ll discuss in chapter 9, as well as in-memory caches. In these cases, it’s essential that the implementations are thread-safe.

让我们仔细看看内存中的存储库。接下来我们将探讨一个例子。

Let’s take a closer look at an in-memory Repository. We’ll explore an example of this next.

示例:使用线程安全的内存存储库

Example: Using a thread-safe in-memory Repository

让我们再次将注意力转移到实现7.3.1 和 8.1.2 节中的类似内容。您可以使用线程安全的内存中实现,而不是使用基于 SQL Server 的。为了使内存数据存储有意义,它必须在所有请求之间共享,因此它必须是线程安全的。如图 8.7所示。CommerceControllerActivatorIProductRepository

Let’s once more turn our attention to implementing a CommerceControllerActivator like those from sections 7.3.1 and 8.1.2. Instead of using a SQL Server–based IProductRepository, you could use a thread-safe, in-memory implementation. For an in-memory data store to make sense, it must be shared among all requests, so it has to be thread-safe. This is illustrated in figure 8.7.

08-07.eps

图 8.7 当运行在不同线程上的多个ProductService实例访问共享资源时,例如内存中IProductRepository,您必须确保共享资源是线程安全的。

Figure 8.7 When multiple ProductService instances running on separate threads access a shared resource, such as an in-memory IProductRepository, you must ensure that the shared resource is thread-safe.

与其使用单例设计模式显式地实现这样的存储库,不如使用一个具体的类,并使用单例生活方式适当地限定它的范围。下一个清单显示了Composer如何在每次被要求解析 a 时返回新实例HomeController,而IProductRepository在所有实例之间共享。

Instead of explicitly implementing such a Repository using the Singleton design pattern, you should use a concrete class and scope it appropriately using the Singleton Lifestyle. The next listing shows how a Composer can return new instances every time it’s asked to resolve a HomeController, whereas IProductRepository is shared among all instances.

清单 8.10 管理单身人士的生活方式

Listing 8.10 Managing a Singleton Lifestyle

public class CommerceControllerActivator : IControllerActivator
{
    private readonly IUserContext userContext;    ①  
    private readonly IProductRepository repository;    ①  

    public CommerceControllerActivator()
    {
        this.userContext = new FakeUserContext();    ②  
        this.repository = new InMemoryProductRepository();    ②  
    }
    ...
    private HomeController CreateHomeController()
    {
        return new HomeController(    ③  
            new ProductService(    ③  
                this.repository,    ③  
                this.userContext));    ③  
    }
}

请注意,在此示例中,repository和都userContext包含Singleton Lifestyles。但是,您可以根据需要混合生活方式图 8.8显示了运行时发生的情况。CommerceControllerActivator

Note that in this example, both repository and userContext encompass Singleton Lifestyles. You can, however, mix Lifestyles if you want. Figure 8.8 shows what happens with CommerceControllerActivator at runtime.

08-08.eps

8.8使用 _CommerceControllerActivator

Figure 8.8 Composing Singletons using CommerceControllerActivator

单身生活方式是最容易实现的生活方式之一。它所需要的只是您保留对该对象的引用并在每次请求时提供相同的对象。在Composer超出范围之前,实例不会超出范围。发生这种情况时,如果对象是一次性类型,则Composer应该处置该对象。

The Singleton Lifestyle is one of the easiest Lifestyles to implement. All it requires is that you keep a reference to the object and serve the same object every time it’s requested. The instance doesn’t go out of scope until the Composer goes out of scope. When that happens, the Composer should dispose of the object if it’s a disposable type.

另一种易于实现的生活方式瞬态生活方式。让我们接下来看看。

Another Lifestyle that’s trivial to implement is the Transient Lifestyle. Let’s look at that next.

8.3.2 短暂的生活方式

8.3.2 The Transient Lifestyle

Transient Lifestyle涉及在每次请求时返回一个新实例。除非实例返回 implements IDisposable,否则没有什么可跟踪的。相反,当实例实现时IDisposableComposer必须牢记它,并在要求释放适用的对象图时显式处置它。本书中构造对象图的大多数示例都隐含地使用了Transient Lifestyle

The Transient Lifestyle involves returning a new instance every time it’s requested. Unless the instance returned implements IDisposable, there’s nothing to keep track of. Conversely, when the instance implements IDisposable, the Composer must keep it in mind and explicitly dispose of it when asked to release the applicable object graph. Most of the examples in this book of constructed object graphs implicitly used the Transient Lifestyle.

值得注意的是,在桌面和类似应用程序中,我们倾向于只解析整个对象层次结构一次:在应用程序启动时。这意味着即使对于Transient组件,也只能创建几个实例,并且它们可以存在很长时间。在每个Dependency只有一个消费者的退化情况下,解析纯Transient组件图的最终结果等同于解析纯Singletons或其任何组合的图。这是因为图只解析了一次,所以行为上的差异从未被意识到。

It’s worth noting that in desktop and similar applications, we tend to resolve the entire object hierarchy only once: at application startup. This means that even for Transient components, only a few instances could be created, and they can be around for a long time. In the degenerate case where there’s only one consumer per Dependency, the end result of resolving a graph of pure Transient components is equivalent to resolving a graph of pure Singletons, or any mix thereof. This is because the graph is resolved only once, so the difference in behavior is never realized.

何时使用瞬态生活方式

When to use the Transient Lifestyle

瞬态生活方式是最安全的生活方式选择,也是效率最低的生活方式之一。它可能会导致创建大量实例并收集垃圾,即使单个实例就足够了。

The Transient Lifestyle is the safest choice of Lifestyles, but also one of the least efficient. It can cause a myriad of instances to be created and garbage collected, even when a single instance would have sufficed.

但是,如果您对组件的线程安全性有疑问,那么Transient Lifestyle是安全的,因为每个消费者都有自己的Dependency实例。在许多情况下,您可以安全地将Transient Lifestyle交换为Scoped Lifestyle,其中也保证对Dependency的访问是顺序的。

If you have doubts about the thread-safety of a component, however, the Transient Lifestyle is safe, because each consumer has its own instance of the Dependency. In many cases, you can safely exchange the Transient Lifestyle for a Scoped Lifestyle, where access to the Dependency is also guaranteed to be sequential.

示例:解析多个存储库

Example: Resolving multiple Repositories

你在本章前面看到了几个使用短暂生活方式的例子。清单 8.3中,存储库是在解析方法中当场创建和注入的,而Composer不保留对它的引用。在清单 8.8 和 8.9 中,您随后看到了如何处理Transient一次性组件。

You saw several examples of using the Transient Lifestyle earlier in this chapter. In listing 8.3, the Repository is created and injected on the spot in the resolving method, and the Composer keeps no reference to it. In listings 8.8 and 8.9, you subsequently saw how to deal with a Transient disposable component.

在这些示例中,您可能已经注意到自始至终userContext都是单例。这是一个纯无状态的服务,所以没有理由为每个ProductService创建的实例创建一个新实例。值得注意的一点是,您可以将Dependencies与不同的Lifestyles混合使用。

In these examples, you may have noticed that the userContext stays a Singleton throughout. This is a purely stateless service, so there’s no reason to create a new instance for every ProductService created. The noteworthy point is that you can mix Dependencies with different Lifestyles.

当多个组件需要相同的Dependency时,每个组件都会被赋予一个单独的实例。以下清单显示了解析 ASP.NET Core MVC 控制器的方法。

When multiple components require the same Dependency, each is given a separate instance. The following listing shows a method resolving an ASP.NET Core MVC controller.

清单 8.11 解析Transient实例AspNetUserContextAdapter

Listing 8.11 Resolving TransientAspNetUserContextAdapter instances

private HomeController CreateHomeController()
{
    return new HomeController(
        new ProductService(
            new SqlProductRepository(this.connStr),
            new AspNetUserContextAdapter(),    ①  
            new SqlUserRepository(
                this.connStr,
                new AspNetUserContextAdapter())));    ①  
}

Transient Lifestyle意味着每个消费者都会收到Dependency的私有实例,即使同一对象图中的多个消费者具有相同的Dependency(如前一个清单中的情况)。如果许多消费者共享相同的Dependency,这种方法可能效率低下;但是如果实现不是线程安全的,那么更高效的Singleton Lifestyle是不合适的。在这种情况下,Scoped Lifestyle可能更合适。

The Transient Lifestyle implies that every consumer receives a private instance of the Dependency, even when multiple consumers in the same object graph have the same Dependency (as is the case in the previous listing). If many consumers share the same Dependency, this approach can be inefficient; but if the implementation isn’t thread-safe, the more efficient Singleton Lifestyle is inappropriate. In such cases, the Scoped Lifestyle may be a better fit.

8.3.3 有范围的生活方式

8.3.3 The Scoped Lifestyle

作为 Web 应用程序的用户,我们希望应用程序尽快做出响应,即使其他用户同时访问该系统也是如此。我们不希望我们的请求与所有其他用户的请求一起放入队列中。如果我们之前有很多请求,我们可能不得不等待过多的时间才能得到响应。为了解决这个问题,Web 应用程序并发处理请求。ASP.NET Core 基础结构通过让每个请求在其自己的上下文中并使用其自己的控制器实例(如果您使用 ASP.NET Core MVC)执行,从而使我们免受此影响。

As users of a web application, we’d like a response from the application as quickly as possible, even when other users are accessing the system at the same time. We don’t want our request to be put on a queue together with all the other users’ requests. We might have to wait an inordinate amount of time for a response if there are many requests ahead of ours. To address this issue, web applications handle requests concurrently. The ASP.NET Core infrastructure shields us from this by letting each request execute in its own context and with its own instance of controllers (if you use ASP.NET Core MVC).

由于并发性,非线程安全的依赖项不能用作单例。另一方面,如果您需要在同一请求中的不同消费者之间共享依赖关系,则将它们用作瞬态可能效率低下甚至是彻头彻尾的问题。

Because of concurrency, Dependencies that aren’t thread-safe can’t be used as Singletons. On the other hand, using them as Transients can be inefficient or even downright problematic if you need to share a Dependency between different consumers within the same request.

尽管 ASP.NET Core 引擎异步执行单个请求,并且单个请求的执行通常涉及多个线程,但它确实保证代码以顺序方式执行 - 至少当您正确执行await异步操作时。8  这意味着如果您可以在单个请求中共享依赖项,线程安全就不是问题。第 8.4.3 节提供了有关异步、多线程方法如何在 ASP.NET Core 中工作的更多详细信息。

Although the ASP.NET Core engine executes a single request asynchronously, and the execution of a single request typically involves multiple threads, it does guarantee that code is executed in a sequential manner — at least when you properly await asynchronous operations.8  This means that if you can share a Dependency within a single request, thread-safety isn’t an issue. Section 8.4.3 provides more details on how the asynchronous, multi-threaded approach works in ASP.NET Core.

尽管 Web 请求的概念仅限于 Web 应用程序和 Web 服务,但请求的概念更为广泛。大多数长时间运行的应用程序使用请求来执行单个操作。例如,当构建一个一个一个地处理队列中的项目的服务应用程序时,您可以将每个处理的项目想象成一个单独的请求,由它自己的一组Dependencies组成。

Although the concept of a web request is limited to web applications and web services, the concept of a request is broader. Most long-running applications use requests to execute single operations. For example, when building a service application that processes items one by one from a queue, you can imagine each processed item as an individual request, consisting of its own set of Dependencies.

这同样适用于桌面或电话应用程序。尽管顶级根类型(视图或 ViewModel)可能会存在很长时间,但您可以将按钮按下视为请求,并且您可以确定此操作的范围并为其提供自己的隔离气泡和自己的一组Dependencies。这导致了Scoped Lifestyle的概念,您可以在其中决定在给定范围内重用实例。图 8.9演示了Scoped Lifestyle的工作原理。

The same could hold for desktop or phone applications. Although the top root types (views or ViewModels) could potentially live for a long time, you could see a button press as a request, and you could scope this operation and give it its own isolated bubble with its own set of Dependencies. This leads to the concept of a Scoped Lifestyle, where you decide to reuse instances within a given scope. Figure 8.9 demonstrates how the Scoped Lifestyle works.

08-09.eps

8.9 Scoped Lifestyle表示您最多为每个指定范围创建一个实例。

Figure 8.9 The Scoped Lifestyle indicates that you create, at most, one instance per specified scope.

请注意,DI 容器可能有专门针对特定技术的Scoped Lifestyle版本。此外,任何一次性组件都应在范围结束时处理。

Note that DI Containers might have specialized versions of the Scoped Lifestyle that target a specific technology. Also, any disposable components should be disposed of when the scope ends.

何时使用Scoped Lifestyle

When to use the Scoped Lifestyle

Scoped Lifestyle对于长期运行的应用程序很有意义,这些应用程序的任务是处理需要以某种程度的隔离运行的操作。当并行处理这些操作时,或者当每个操作都包含自己的状态时,就需要隔离。Web 应用程序是Scoped Lifestyle运作良好的一个很好的例子,因为 Web 应用程序通常并行处理请求,而这些请求通常包含一些特定于请求的可变状态。但是,即使 Web 应用程序启动了一些与 Web 请求无关的后台操作,Scoped Lifestyle也是有价值的。甚至这些后台操作通常也可以映射到请求的概念。

The Scoped Lifestyle makes sense for long-running applications that are tasked with processing operations that need to run with some degree of isolation. Isolation is required when these operations are processed in parallel, or when each operation contains its own state. Web applications are a great example of where the Scoped Lifestyle works well, because web applications typically process requests in parallel, and those requests typically contain some mutable state that’s specific to the request. But even if a web application starts some background operation that isn’t related to a web request, the Scoped Lifestyle is valuable. Even these background operations can typically be mapped to the concept of a request.

与所有Lifestyles一样,您可以将Scoped Lifestyle与其他生活方式混合使用,例如,某些依赖项被配置为Singletons,而其他依赖项则按请求共享。

As with all Lifestyles, you can mix the Scoped Lifestyle with others so that, for example, some Dependencies are configured as Singletons, and others are shared per request.

示例:使用作用域 DbContext 编写长时间运行的应用程序

Example: Composing a long-running application using a scoped DbContext

在此示例中,您将了解如何使用范围限定的DbContext Dependency组合长时间运行的控制台应用程序。这个控制台应用程序是我们在 7.1 节中讨论的 UpdateCurrency 程序的变体。

In this example, you’ll see how to compose a long-running console application with a scoped DbContext Dependency. This console application is a variation of the UpdateCurrency program we discussed in section 7.1.

与 UpdateCurrency 程序一样,这个新的控制台应用程序读取货币汇率。然而,这个版本的目标是每分钟输出一次特定货币金额的汇率,并继续这样做,直到用户停止应用程序。图 8.10概述了应用程序的主要类。

Just as with the UpdateCurrency program, this new console application reads currency exchange rates. The goal of this version, however, is to output the exchange rates of a particular currency amount once a minute and to continue to do so until the user stops the application. Figure 8.10 outlines the application’s main classes.

08-10.eps

图 8.10 CurrencyMonitoring 程序的类图

Figure 8.10 The class diagram of the CurrencyMonitoring program

CurrencyMonitoring 程序重用了第 7 章的 UpdateCurrency 程序中的 和 以及SqlExchangeRateProvider第4 章中的抽象。抽象及其伴随的实现是新的。这也是新的并且特定于该程序;它显示在以下清单中。CommerceContextICurrencyConverter ICurrencyRepository SqlCurrencyRepositoryCurrencyRateDisplayer

The CurrencyMonitoring program reuses the SqlExchangeRateProvider and CommerceContext from the UpdateCurrency program of chapter 7 and the ICurrencyConverter Abstraction from chapter 4. The ICurrencyRepository Abstraction and its accompanying SqlCurrencyRepository implementation are new. The CurrencyRateDisplayer is also new and is specific to this program; it’s shown in the following listing.

清单CurrencyRateDisplayer8.12 类

Listing 8.12 The CurrencyRateDisplayer class

public class CurrencyRateDisplayer
{
    private readonly ICurrencyRepository repository;
    private readonly ICurrencyConverter converter;

    public CurrencyRateDisplayer(
        ICurrencyRepository repository,
        ICurrencyConverter converter)
    {
        this.repository = repository;
        this.converter = converter;
    }

    public void DisplayRatesFor(Money amount)
    {
        Console.WriteLine(
            "Exchange rates for {0} at {1}:",
            amount,
            DateTime.Now);

        IEnumerable<Currency> currencies =
            this.repository.GetAllCurrencies();    ①  

        foreach (Currency target in currencies)
        {
            Money rate = this.converter.Exchange(    ②  
                amount,    ②  
                target);    ②  

            Console.WriteLine(rate);    ③  
        }
    }
}

"EUR 1.00"您可以使用as 参数从命令行运行应用程序。这样做会输出以下文本:

You can run the application from the command line using "EUR 1.00" as argument. Doing so outputs the following text:

Exchange rates for EUR 1.00000 at 12/10/2018 22:55:00.
CAD 1.48864
USD 1.13636
DKK 7.46591
EUR 1.00000
GBP 0.89773

要将应用程序拼凑在一起,您需要创建应用程序的Composition Root。在这种情况下,组合根由两个类组成,如图 8.11所示。

To piece the application together, you need to create the application’s Composition Root. The Composition Root, in this case, consists of two classes, as shown in figure 8.11.

08-11.eps

图 8.11 应用程序的基础结构由两个类组成,ProgramComposer

Figure 8.11 The application’s infrastructure consists of two classes, Program and Composer.

Program班级_使用Composer解析应用程序的对象图。清单 8.13显示了Composer该类及其CreateRateDisplayer方法. 它确保对于每个解析,只创建范围CommerceContext 依赖项的一个实例。

The Program class uses the Composer class to resolve the application’s object graph. Listing 8.13 shows the Composer class with its CreateRateDisplayer method. It ensures that for each resolve, only one instance of the scoped CommerceContext Dependency is created.

清单Composer8.13 类,负责组成对象图

Listing 8.13 The Composer class, responsible for composing object graphs

public class Composer
{
    private readonly string connectionString;    ①  

    public Composer(string connectionString)
    {
        this.connectionString = connectionString;
    }

    public CurrencyRateDisplayer CreateRateDisplayer()    ②  
    {
        var context =    ③  
            new CommerceContext(this.connectionString);  ③  

        return new CurrencyRateDisplayer(
            new SqlCurrencyRepository(
                context),    ④  
            new CurrencyConverter(
                new SqlExchangeRateProvider(
                    context)));    ④  
    }
}

Composition Root的其余部分是应用程序的入口点:Program类。它负责读取输入参数和配置文件,并设置Timer每分钟运行一次以显示汇率。以下清单充分展示了它。

The remaining part of the Composition Root is the application’s entry point: the Program class. It’s responsible for reading the input arguments and configuration file, and setting up the Timer that runs once a minute to display exchange rates. The following listing shows it in full glory.

清单 8.14 管理范围的应用程序入口点

Listing 8.14 The application’s entry point that manages scopes

public static class Program
{
    private static Composer composer;

    public static void Main(string[] args)
    {
        var money = new Money(    ①  
            currency: new Currency(code: args[0]),    ①  
            amount: decimal.Parse(args[1]));    ①  

        composer = new Composer(LoadConnectionString());

        var timer = new Timer(interval: 60000);    ②  
        timer.Elapsed += (s, e) => DisplayRates(money);  ②  
        timer.Start();    ②  

        Console.WriteLine("Press any key to exit.");
        Console.ReadLine();    ③  
    }

    private static void DisplayRates(Money money)
    {
        CurrencyRateDisplayer displayer =
            composer.CreateRateDisplayer();    ④  

        displayer.DisplayRatesFor(money);
    }

    private static string LoadConnectionString() { ... }
}

该类Program配置一个Timer调用该DisplayRates方法的当它过去时。即使您DisplayRates每分钟只调用一次,但在此示例中,您可以轻松地DisplayRates通过多个线程并行调用,甚至进行DisplayRates异步调用。这仍然有效,因为每个调用都会创建和管理其范围内的实例集,从而允许每个操作独立于其他操作运行。

The Program class configures a Timer that calls the DisplayRates method when it elapses. Even though you only call DisplayRates once per minute, in this example, you could easily call DisplayRates in parallel over multiple threads or even make DisplayRates asynchronous. This would still work because each call creates and manages its set of scoped instances, allowing each operation to run in isolation from the others.

Transient Lifestyle意味着每个消费者都会收到一个Dependency的私有实例,而Scoped Lifestyle确保该范围内所有解析图的所有消费者都获得相同的实例。除了常见的Lifestyle模式,例如SingletonTransientScoped,还有一些模式可以定义为代码味道甚至反模式。以下部分将讨论其中一些糟糕的生活方式选择。

Whereas a Transient Lifestyle implies that every consumer receives a private instance of a Dependency, a Scoped Lifestyle ensures that all consumers of all resolved graphs for that scope get the same instance. Besides common Lifestyle patterns, such as Singleton, Transient, and Scoped, there are also patterns that you can define as code smells or even anti-patterns. A few of those bad Lifestyle choices are discussed in the following section.

8.4 不良生活方式的选择

8.4 Bad Lifestyle choices

众所周知,某些生活方式的选择不利于我们的健康,吸烟就是其中之一。在 DI中应用生活方式时也是如此。你会犯很多错误。在本节中,我们将讨论表 8.2中显示的选项。

As we all know, some lifestyle choices are bad for our heath, smoking being one of them. The same holds true when it comes to applying Lifestyles in DI. You can make many mistakes. In this section, we discuss the choices shown in table 8.2.

表 8.2本节涵盖的 不良生活方式选择
学科类型描述
俘虏依赖漏洞使依赖项在其预期生命周期之外仍被引用
泄漏的抽象设计问题使用Leaky Abstractions ,将生活方式的选择泄露给消费者
每线程生活方式漏洞通过将实例绑定到线程的生命周期来导致并发错误

表 8.2所述,Captive Dependencies和 per-thread Lifestyle可能会导致您的应用程序出现错误。通常,这些错误仅在将应用程序部署到生产环境后才会出现,因为它们与并发相关。当我们启动应用程序时,作为开发人员,我们通常会运行它一小段时间,一次一个请求。这同样适用于通常以有序方式检查应用程序的测试人员。这可能会隐藏此类问题,这些问题仅在多个用户同时访问应用程序时才会弹出。

As table 8.2 states, Captive Dependencies and the per-thread Lifestyle can cause bugs in your application. More often than not, these bugs only appear after deploying the application to production, because they are concurrency related. When we start the application, as developers, we typically run it for a short period of time, one request at a time. The same holds true for testers that typically go through the application in an orderly fashion. This might hide such problems, which only pop up when multiple users access the application concurrently.

当我们向消费者泄露我们的生活方式选择的细节时,这通常不会导致错误——或者至少不会立即导致错误。但是,它确实会使Dependency的消费者及其测试复杂化,并可能导致整个代码库发生彻底的变化。最后,这增加了错误的机会。

When we leak details of our Lifestyle choices to our consumers, this typically won’t lead to bugs — or at least, not immediately. It does, however, complicate the Dependency’s consumers and their tests, and might cause sweeping changes throughout the code base. In the end, this increases the chance of bugs.

8.4.1 俘虏依赖

8.4.1 Captive Dependencies

在生命周期管理方面,一个常见的陷阱是Captive Dependencies。当依赖项被消费者保持存活的时间比您预期的要长时,就会发生这种情况。这甚至可能导致它被多个线程或并发请求重用,即使依赖项不是线程安全的。

When it comes to lifetime management, a common pitfall is that of Captive Dependencies. This happens when a Dependency is kept alive by a consumer for longer than you intended it to be. This might even cause it to be reused by multiple threads or requests concurrently, even though the Dependency isn’t thread-safe.

Captive Dependency的一个非常常见的示例是将短暂的依赖项注入到Singleton消费者中。Singleton在Composer的生命周期内保持活动状态,它的Dependency也是如此。下面的清单说明了这个问题。

An all-too-common example of a Captive Dependency is when a short-lived Dependency is injected into a Singleton consumer. A Singleton is kept alive for the lifetime of the Composer, and so will its Dependency. The following listing illustrates this problem.

坏.tif

清单 8.15 俘虏依赖示例

Listing 8.15 Captive Dependency example

public class Composer
{
    private readonly IProductRepository repository;

    public Composer(string connectionString)
    {
        this.repository = new SqlProductRepository(    ①  
            new CommerceContext(connectionString));    ②  
    }
    ...
}

SqlProductRepository因为整个应用程序只有一个实例,并且在其私有字段中CommerceContext被引用SqlProductRepository,所以实际上也只有一个实例CommerceContext。这是一个问题,因为CommerceContext它不是线程安全的,也不打算超过单个请求。因为在超过其预期发布时间之前CommerceContext一直被俘虏,我们称之为俘虏依赖SqlProductRepositoryCommerceContext

Because there’s only one instance of SqlProductRepository for the entire application, and CommerceContext is referenced by SqlProductRepository in its private field, there will be effectively just one instance of CommerceContext too. This is a problem, because CommerceContext isn’t thread-safe and isn’t intended to outlive a single request. Because CommerceContext is kept captive by SqlProductRepository past its expected release time, we call CommerceContext a Captive Dependency.

使用DI Container时, Captive Dependencies是一个常见问题。这是由DI 容器的动态特性引起的,它很容易忘记您正在构建的对象图的形状。然而,正如前面的示例所示,使用Pure DI时也会出现问题。通过仔细构建Pure DI Composition Root中的代码,您可以减少遇到此问题的机会。下面的清单显示了这种方法的一个例子。

Captive Dependencies are a common problem when you’re working with a DI Container. This is caused by the dynamic nature of DI Containers that make it easy to lose track of the shape of the object graphs you’re building. As the previous example showed, however, the problem can also arise when working with Pure DI. By carefully structuring code in the Pure DI Composition Root, you can reduce the chance of running into this problem. The following listing shows an example of this approach.

清单 8.16使用纯 DI 减轻强制依赖

Listing 8.16 Mitigating Captive Dependencies with Pure DI

public class CommerceControllerActivator : IControllerActivator
{
    private readonly string connStr;    ①  
    private readonly IUserContext userContext;    ①  

    public CommerceControllerActivator(string connectionString)
    {
        this.connStr = connectionString;    ②  
        this.userContext =    ②  
            new AspNetUserContextAdapter();    ②  
    }

    public object Create(ControllerContext ctx)
    {
        var context = new CommerceContext(this.connStr);    ③  
        var provider = new SqlExchangeRateProvider(context);    ③  
    ③  

        Type type = ctx.ActionDescriptor
            .ControllerTypeInfo.AsType();

        if (type == typeof(HomeController))
        {
            return this.CreateHomeController(context);    ④  
        }
        else if (type == typeof(ExchangeController))
        {
            return this.CreateExchangeController(
                context, provider);    ④  
        }
        else
        {
            throw new Exception("Unknown controller " + type.Name);
        }
    }

    private HomeController CreateHomeController(
        CommerceContext context)
    {
        return new HomeController(    ⑤  
            new ProductService(    ⑤  
                new SqlProductRepository(    ⑤  
                    context),    ⑤  
                this.userContext));    ⑤  
    }

    private RouteController CreateExchangeController(
        CommerceContext context,
        IExchangeRateProvider provider) { ... }
}

清单 8.16将所有依赖项的创建分为三个不同的阶段。当您将这些阶段分开时,检测和防止Captive Dependencies变得更加容易。这些阶段是

Listing 8.16 separates the creation of all Dependencies into three distinct phases. When you separate these phases, it becomes much easier to detect and prevent Captive Dependencies. These phases are

  • 应用程序启动期间创建的单例
  • Singletons created during application start-up
  • 在请求开始时创建的范围实例
  • Scoped instances created at the start of a request
  • 根据请求,由TransientScopedSingleton实例组成的特定对象图
  • Based on the request, a particular object graph that consists of Transient, Scoped, and Singleton instances

使用此模型,为每个请求创建应用程序的所有作用域依赖项,即使它们未被使用。这可能看起来效率低下,但请记住,正如我们在 4.2.2 节中讨论的那样,组件构造函数应该不受除保护检查和存储传入依赖项之外的所有逻辑的影响。这使得构建速度更快,并防止了大多数性能问题;创建一些未使用的依赖项不是问题。

With this model, all the application’s Scoped Dependencies are created for each request, even when they aren’t used. This might seem inefficient, but remember that, as we discussed in section 4.2.2, component constructors should be free from all logic except guard checks and when storing incoming Dependencies. This makes construction fast and prevents most performance issues; the creation of a few unused Dependencies is a non-issue.

从错误配置的角度来看,Captive Dependencies是与不良生活方式选择相关的最常见、最难发现的配置或编程错误之一。比我们愿意承认的更多的时候,我们已经浪费了很多时间来寻找由Captive Dependencies引起的错误。这就是为什么我们认为工具支持在您使用DI 容器时发现Captive Dependencies是无价的。尽管强制依赖通常是由配置或编程错误引起的,但其他不方便的生活方式选择是设计缺陷,例如当您将生活方式选择强加给消费者时。

From a misconfiguration perspective, Captive Dependencies are one of the most common, hardest-to-spot configurations or programming errors related to bad Lifestyle choices. More often than we’d like to admit, we’ve wasted many hours trying to find bugs caused by Captive Dependencies. That’s why we consider tool support for spotting Captive Dependencies invaluable when you’re using a DI Container. Although Captive Dependencies are typically caused by configuration or programming errors, other inconvenient Lifestyle choices are design flaws, such as when you’re forcing Lifestyle choices on consumers.

8.4.2 使用Leaky Abstractions向消费者泄露生活方式选择

8.4.2 Using Leaky Abstractions to leak Lifestyle choices to consumers

另一种可能最终导致错误的生活方式选择的情况是,当您需要推迟创建Dependency时。当您有一个很少需要且创建成本很高的依赖项时,您可能更愿意在组合对象图之后动态创建这样的实例。这是一个合理的担忧。然而,并没有将这种担忧推给Dependency的消费者。如果您这样做,您将向消费者泄露有关组合根的实现和实现选择的详细信息。依赖成为一个有漏洞的抽象,你违反了依赖倒置原则

Another case where you might end up with a bad Lifestyle choice is when you need to postpone the creation of a Dependency. When you have a Dependency that’s rarely needed and is costly to create, you might prefer to create such an instance on the fly, after the object graph is composed. This is a valid concern. What isn’t, however, is pushing such a concern on to the Dependency’s consumers. If you do this, you’re leaking details about the implementation and implementation choices of the Composition Root to the consumer. The Dependency becomes a Leaky Abstraction, and you’re violating the Dependency Inversion Principle.

在本节中,我们将展示两个常见示例,说明如何将您的生活方式选择泄露给Dependency的消费者。这两个示例都有相同的解决方案:创建一个包装类来隐藏Lifestyle选项和函数作为原始抽象的实现而不是Leaky Abstraction

In this section, we’ll show two common examples of how you can cause your Lifestyle choice to be leaked to a Dependency’s consumer. Both examples have the same solution: create a wrapper class that hides the Lifestyle choice and functions as an implementation of the original Abstraction rather than the Leaky Abstraction.

Lazy<T>作为一个有漏洞的抽象

Lazy<T> as a Leaky Abstraction

让我们再次回到ProductService清单 3.9 中首次引入的经常重复使用的示例。假设其依赖项之一的创建成本很高,并且并非应用程序中的所有代码路径都需要它的存在。

Let’s again return to our regularly reused ProductService example that was first introduced in listing 3.9. Let’s imagine that one of its Dependencies is costly to create, and not all code paths in the application require its existence.

这是您可能想通过使用 .NET 的System.Lazy<T>类来解决的问题. A允许通过其属性Lazy<T>访问基础值。Value但是,该值只会在第一次请求时创建。之后,Lazy<T>只要Lazy<T>实例存在,就会缓存该值。

This is something you might be tempted to solve by using .NET’s System.Lazy<T> class. A Lazy<T> allows access to an underlying value through its Value property. That value, however, will only be created when it’s requested for the first time. After that, the Lazy<T> caches the value for as long as the Lazy<T> instance exists.

这很有用,因为它允许您延迟Dependencies的创建。然而,Lazy<T>直接注入消费者的构造函数是错误的,我们稍后会讨论。下一个清单显示了此类错误使用Lazy<T>.

This is useful, because it allows you to delay the creation of Dependencies. It’s an error, however, to inject Lazy<T> directly into a consumer’s constructor, as we’ll discuss later. The next listing shows an example of such an erroneous use of Lazy<T>.

坏.tif

清单 8.17 Lazy<T>作为有漏洞的抽象

Listing 8.17 Lazy<T> as Leaky Abstraction

public class ProductService : IProductService
{
    private readonly IProductRepository repository;
    private readonly Lazy<IUserContext> userContext;   

    public ProductService(
        IProductRepository repository,
        Lazy<IUserContext> userContext)  ①      
    {
        this.repository = repository;
        this.userContext = userContext;
    }

    public IEnumerable<DiscountedProduct> GetFeaturedProducts()
    {
        return
            from product in this.repository
                .GetFeaturedProducts()
            select product.ApplyDiscountFor(
                this.userContext.Value);    ②  
    }
}

清单 8.18显示了清单 8.17 的Composition Root结构。ProductService

Listing 8.18 shows the structure of the Composition Root for the ProductService of listing 8.17.

坏.tif

清单 8.18 组成一个ProductService依赖于Lazy<IUserContext>

Listing 8.18 Composing a ProductService that depends on Lazy<IUserContext>

Lazy<IUserContext> lazyUserContext =
    new Lazy<IUserContext>(    ①  
        () => new AspNetUserContextAdapter())

new HomeController(
    new ProductService(
        new SqlProductRepository(
            new CommerceContext(connectionString)),
        lazyUserContext));    ②  

看到这段代码后,您可能想知道它有什么不好。下面的讨论列出了这种设计的几个问题,但重要的是您要知道在组合根Lazy<T>内部使用没有任何问题——然而,注入到应用程序组件中会导致抽象泄漏。现在,回到问题。Lazy<T>

After seeing this code, you might wonder what’s so bad about it. The following discussion lists several problems with such a design, but it’s important that you know there’s nothing wrong with the use of Lazy<T> inside your Composition Root — injecting Lazy<T> into an application component, however, leads to Leaky Abstractions. Now, back to the problems.

首先,让消费者依赖Lazy<IUserContext>会使消费者及其单元测试复杂化。您可能认为必须调用是能够延迟加载昂贵的DependencyuserContext.Value的一个小代价,但事实并非如此。创建单元测试时,您不仅必须创建包装原始Dependency的实例,而且还必须编写额外的测试来验证它是否未在错误的时间被调用。Lazy<T>Value

First, letting a consumer depend on Lazy<IUserContext> complicates the consumer and its unit tests. You might think that having to call userContext.Value is a small price to pay for being able to lazy load an expensive Dependency, but it isn’t. When creating unit tests, not only do you have to create Lazy<T> instances that wrap the original Dependency, but you also have to write extra tests to verify whether that Value isn’t being called at the wrong time.

因为使Dependency惰性作为性能优化似乎很重要,所以不验证您是否正确实现它会很奇怪。这至少是您需要为该Dependency的每个消费者编写的一个额外测试。这样的Dependency可能有几十个消费者,他们都需要额外的测试来验证他们的正确性。

Because making the Dependency lazy seems important enough as a performance optimization, it would be weird not to verify whether you implemented it correctly. This is, at least, one extra test you need to write for every consumer of that Dependency. There might be dozens of consumers for such a Dependency, and they all need the extra tests to verify their correctness.

其次,在开发过程的后期将现有依赖项更改为惰性依赖项会导致整个应用程序发生彻底的变化。当该Dependency有数十个消费者时,这可能会带来很大的工作量,因为正如上一点所讨论的,不仅消费者本身需要更改,而且他们的所有测试也需要更改。做出这些连锁反应既费时又冒险。

Second, changing an existing Dependency to a lazy Dependency later in the development process causes sweeping changes throughout the application. This can present a serious amount of effort when there are dozens of consumers for that Dependency, because, as discussed in the previous point, not only do the consumers themselves need to be altered, but all of their tests need to be changed too. Making these kinds of rippling changes is time consuming and risky.

为防止这种情况,您可以将所有依赖项默认设为惰性,因为从理论上讲,每个依赖项将来都可能变得昂贵。这将避免您将来必须进行任何级联更改。但这太疯狂了,我们希望您同意这不是一条好的追求之路。如果您认为每个Dependency都可能成为一个实现列表,则尤其如此,我们将在稍后讨论。这将导致默认设置所有依赖项 IEnumerable<Lazy<T>>,这将更加疯狂。

To prevent this, you could make all Dependencies lazy by default, because, in theory, every Dependency could potentially become expensive in the future. This would prevent you from having to make any future cascading changes. But this would be madness, and we hope you agree that this isn’t a good path to pursue. This is especially true if you consider that every Dependency could potentially become a list of implementations, as we’ll discuss shortly. This would lead to making all Dependencies IEnumerable<Lazy<T>> by default, which would be, even more so, insane.

最后,由于必须进行的更改数量和需要添加的测试数量,很容易出现编程错误,从而使这些更改完全无效。例如,如果您创建一个意外依赖IUserContext而不是依赖的新组件Lazy<IUserContext>,则意味着包含该组件的每个图都将始终获得一个急切加载的IUserContext实现。

Last, because the amount of changes you have to make and the number of tests you need to add, it becomes quite easy to make programming mistakes that would completely nullify these changes. For instance, if you create a new component that accidentally depends on IUserContext instead of Lazy<IUserContext>, it means that every graph that contains that component will always get an eagerly loaded IUserContext implementation.

不过,这并不意味着不允许您懒惰地构建依赖项。然而,我们想重复我们在 4.2.1 节中的陈述:除了 Guard Clauses 和传入依赖项的存储之外,你应该让你的组件的构造函数没有任何逻辑。这使得您的类的构造快速且可靠,并且将防止此类组件的实例化变得昂贵。

This doesn’t mean that you aren’t allowed to construct your Dependencies lazily, though. We’d like, however, to repeat our statement from section 4.2.1: you should keep the constructors of your components free of any logic other than Guard Clauses and the storing of incoming Dependencies. This makes the construction of your classes fast and reliable, and will prevent such components from ever becoming expensive to instantiate.

但是,在某些情况下,您别无选择;例如,在处理第三方组件时,您几乎无法控制。在那种情况下,Lazy<T>是一个很棒的工具。但是,与其让所有消费者依赖Lazy<T>,不如隐藏Lazy<T>在虚拟代理后面,并将该虚拟代理放置在组合根中。11  下面的列表提供了一个例子。

In some cases, however, you’ll have no choice; for instance, when dealing with third-party components you have little control over. In that case, Lazy<T> is a great tool. But rather than letting all consumers depend on Lazy<T>, you should hide Lazy<T> behind a Virtual Proxy and place that Virtual Proxy within the Composition Root.11  The following listing provides an example of this.

好的.tif

清单 8.19 虚拟代理包装Lazy<T>

Listing 8.19 Virtual Proxy wrapping Lazy<T>

public class LazyUserContextProxy : IUserContext    ①  
{
    private readonly Lazy<IUserContext> userContext;    ②  

    public LazyUserContextProxy(
        Lazy<IUserContext> userContext)
    {
        this.userContext = userContext;
    }

    public bool IsInRole(Role role)
    {
        IUserContext real = this.userContext.Value;    ③  
        return real.IsInRole(role);    ③  
    }
}

这个新的允许依赖而不是。这是的新构造函数:LazyUserContextProxyProductServiceIUserContextLazy<IUserContext>ProductService

This new LazyUserContextProxy allows ProductService to dependent on IUserContext instead of Lazy<IUserContext>. Here’s ProductService’s new constructor:

public ProductService(
    IProductRepository repository,
    IUserContext userContext)

下一个清单显示了如何在HomeController注入.LazyUserContextProxyProductService

The next listing shows how you can compose the object graph for HomeController while injecting LazyUserContextProxy into ProductService.

好的.tif

清单 8.20通过注入虚拟代理来 组合 aProductService

Listing 8.20 Composing a ProductService by injecting a Virtual Proxy

IUserContext lazyProxy =
    new LazyUserContextProxy(    ①  
        new Lazy<IUserContext>(    ①  
            () => new AspNetUserContextAdapter()));    ①  

new HomeController(
    new ProductService(
        new SqlProductRepository(
            new CommerceContext(connectionString)),
        lazyProxy));    ②  

清单 8.19所示,拥有一个依赖于 的类本身并不是一件坏事Lazy<T>,但是您想将其集中在Composition Root中并且只有一个类依赖于Lazy<IUserContext>。Depending onFunc<T>与 depending 的效果几乎相同Lazy<T>,解决方案也相似。这样做可以防止你的代码变得复杂,单元测试不会被添加,扫除所做的更改,以及引入的不幸错误。正如您接下来将看到的,相同的论据也适用于注入IEnumerable<T>

As listing 8.19 shows, it’s not a bad thing per se to have a class depending on Lazy<T>, but you want to centralize this inside the Composition Root and only have a single class that takes this dependency on Lazy<IUserContext>. Depending on Func<T> has practically the same effect as depending on Lazy<T>, and the solution is similar. Doing so prevents your code from being complicated, unit tests from being added, sweeping changes from being made, and unfortunate bugs from being introduced. As you’ll see next, the same arguments hold for injecting IEnumerable<T> too.

IEnumerable<T>作为一个有漏洞的抽象

IEnumerable<T> as a Leaky Abstraction

就像使用Lazy<T>延迟创建依赖项一样,在许多情况下,您需要处理某个抽象的依赖项集合。为此,您可以使用 BCL 集合抽象之一,例如. 尽管就其本身而言,使用as Abstraction来呈现一组Dependencies并没有错,但在错误的地方使用它会再次导致Leaky Abstraction。以下清单显示了如何不正确地使用。IEnumerable<T>IEnumerable<T>IEnumerable<T>

Just as with using Lazy<T> to delay the creation of Dependencies, there are many cases where you need to work with a collection of Dependencies of a certain Abstraction. For this purpose, you can make use of one of the BCL collection Abstractions, such as IEnumerable<T>. Although, in itself, there’s nothing wrong with using IEnumerable<T> as an Abstraction to present a collection of Dependencies, using it in the wrong place can, once again, lead to a Leaky Abstraction. The following listing shows how IEnumerable<T> can be used incorrectly.

坏.tif

清单 8.21 IEnumerable<T>作为一个有漏洞的抽象

Listing 8.21 IEnumerable<T> as a Leaky Abstraction

public class Component
{
    private readonly IEnumerable<ILogger> loggers;

    public Component(IEnumerable<ILogger> loggers)    ①  
    {
        this.loggers = loggers;
    }

    public void DoSomething()
    {
        foreach (var logger in this.loggers)    ②  
        {
            logger.Log("DoSomething called");
        }

        ...
    }
}

我们希望避免消费者不得不处理某个Dependency可能有多个实例的事实。IEnumerable<ILogger> 这是通过Dependency泄漏的实现细节。正如我们之前解释的,每个依赖项都可能有多个实现,但您的消费者不需要意识到这一点。与前面的示例一样,当您有多个这样的DependencyLazy<T>消费者时,这种泄漏会增加系统的复杂性和维护成本,因为每个消费者都必须处理集合的循环。消费者测试也是如此。

We’d like to prevent consumers from having to deal with the fact that there might be multiple instances of a certain Dependency. This is an implementation detail that’s leaking out through the IEnumerable<ILogger> Dependency. As we explained previously, every Dependency could potentially have multiple implementations, but your consumers shouldn’t need to be aware of this. Just as with the previous Lazy<T> example, this leakage increases the system’s complexity and maintenance costs when you have multiple consumers of such a Dependency, because every consumer has to deal with looping over the collection. So do consumer’s tests.

foreach尽管经验丰富的开发人员在几秒钟内就吐出这样的构造,但当需要以不同方式处理依赖项集合时,事情会变得更加复杂。例如,假设即使其中一个记录器失败,记录也应该继续:

Although experienced developers spit out foreach constructs like this in a matter of seconds, things get more complicated when the collection of Dependencies needs to be processed differently. For example, let’s say that logging should continue even if one of the loggers fails:

foreach (var logger in this.loggers)
{
    try
    {
        logger.Log("DoSomething called");
    }
    catch    ①  
    {    ①  
    }    ①  
}

或者,也许您不仅要继续处理,还要将该错误记录到下一个记录器。这样,下一个记录器将作为失败记录器的回退:

Or, perhaps you not only want to continue processing, but also log that error to the next logger. This way, the next logger functions as a fallback for the failed logger:

for (int index = 0; index < this.loggers.Count; index++)
{
    try
    {
        this.loggers[index].Log("DoSomething called");    ②  
    }
    catch (Exception ex)
    {
        if (loggers.Count > index + 1)
        {
            loggers[index + 1].Log(ex);    ③  
        }
    }
}

或者也许——好吧,我们认为你明白了。到处都是这些类型的代码结构会很痛苦。如果你想改变你的日志记录策略,它会导致你在整个应用程序中进行级联更改。理想情况下,我们希望将这些知识集中到一个位置。

Or perhaps — well, we think you get the idea. It’d be rather painful to have these kinds of code constructs all over the place. If you want to change your logging strategy, it causes you to make cascading changes throughout the application. Ideally, we’d like to centralize this knowledge to one single location.

您可以使用 Composite 设计模式解决此设计问题。您现在应该熟悉复合设计模式,正如我们在第 1 章和第 6 章中讨论的那样(参见图 1.8,以及清单 6.4 和 6.12)。下一个清单显示了ILogger.

You can fix this design problem using the Composite design pattern. You should be familiar with the Composite design pattern by now, as we’ve discussed it in chapters 1 and 6 (see figure 1.8, and listings 6.4 and 6.12). The next listing shows a Composite for ILogger.

好的.tif

清单 8.22 复合包装IEnumerable<T>

Listing 8.22 Composite wrapping IEnumerable<T>

public class CompositeLogger : ILogger    ①  
{
    private readonly IList<ILogger> loggers;    ②  

    public CompositeLogger(IList<ILogger> loggers)
    {
        this.loggers = loggers;
    }

    public void Log(LogEntry entry)    ③  
    {
        for (int index = 0; index < this.loggers.Count; index++)
        {
            try
            {
                this.loggers[index].Log(entry);
            }
            catch (Exception ex)
            {
                if (loggers.Count > index + 1)
                {
                    var logger = loggers[index + 1];
                    logger.Log(new LogEntry(ex));    ④  
                }
            }
        }
    }
}

下面的代码片段展示了如何Component使用这个新的来组合对象图,保持依赖于一个而不是一个:CompositeLoggerComponentILoggerIEnumerable<ILogger>

The following snippet shows how you can compose the object graph for Component using this new CompositeLogger, keeping Component dependent on a single ILogger instead of an IEnumerable<ILogger>:

好的.tif
ILogger composite =
    new CompositeLogger(new ILogger[]    ①  
    {
        new SqlLogger(connectionString),
        new WindowsEventLogLogger(source: "MyApp"),
        new FileLogger(directory: "c:\\logs")
    });

new Component(composite);    ②  

正如您之前多次看到的那样,良好的应用程序设计遵循依赖倒置原则并防止泄漏抽象。这会产生更清晰的代码,更易于维护并且对编程错误更有弹性。现在让我们看看另一种味道,它本身不会影响应用程序的设计,但可能会导致难以修复的并发问题。

As you’ve seen many times before, good application design follows the Dependency Inversion Principle and prevents Leaky Abstractions. This results in cleaner code that’s more maintainable and more resilient to programming errors. Let’s now look at a different smell, which doesn’t affect the application’s design per se, but potentially causes hard-to-fix concurrency problems.

8.4.3 将实例绑定到线程的生命周期导致并发错误

8.4.3 Causing concurrency bugs by tying instances to the lifetime of a thread

有时您要处理的依赖项不是线程安全的,但不一定需要绑定到请求的生命周期。一个诱人的解决方案是将此类依赖项的生命周期与线程的生命周期同步。虽然诱人,但这种做法很容易出错。

Sometimes you’re dealing with Dependencies that aren’t thread-safe but don’t necessarily need to be tied to the lifetime of a request. A tempting solution is to synchronizing the lifetime of such a Dependency to the lifetime of a thread. Although seductive, such practice is error prone.

清单 8.23显示了该CreateCurrencyParser方法如何,之前在清单 7.2 中讨论过,它使用了一个Dependency。这是为应用程序中的每个线程创建一次。SqlExchangeRateProvider

Listing 8.23 shows how the CreateCurrencyParser method, previously discussed in listing 7.2, makes use of a SqlExchangeRateProvider Dependency. This is created once for each thread in the application.

坏.tif

清单 8.23 Dependency 的生命周期与线程的生命周期相关联

Listing 8.23 A Dependency’s lifetime tied to the lifetime of a thread

[ThreadStatic]    ①  
private static CommerceContext context;    ①  

static CurrencyParser CreateCurrencyParser(
    string connectionString)
{
    if (context == null)    ②  
    {    ②  
        context = new CommerceContext(    ②  
           connectionString);    ②  
    }    ②  

    return new CurrencyParser(    ③  
        new SqlExchangeRateProvider(context),    ③  
        context);    ③  
}

尽管这看起来很无辜,但事实并非如此。接下来我们将讨论此清单的两个问题。

Although this might look innocent, that couldn’t be further from the truth. We’ll discuss two problems with this listing next.

线程的生命周期往往不清楚

The lifetime of a thread is often unclear

很难预测线程的寿命。当您使用创建并启动一个线程new Thread().Start()时,您将获得一个新的线程静态内存块。这意味着如果您调用这样的线程,线程静态字段将全部取消设置,从而导致创建新实例。CreateCurrencyParser

It can be hard to predict what the lifespan of a thread is. When you create and start a thread using new Thread().Start(), you’ll get a fresh block of thread-static memory. This means that if you call CreateCurrencyParser in such a thread, the thread-static fields will all be unset, resulting in new instances being created.

但是,当使用 启动线程池中的线程时,您可能会从池中获取现有线程或新创建的线程,具体取决于线程池中的内容。即使您不是自己创建线程,框架也可能是(正如我们已经讨论过的,例如,ASP.NET Core)。这意味着虽然某些线程的生命周期很短,但其他线程的生命周期会贯穿整个应用程序。当不能保证操作在单个线程上运行时,会出现更复杂的情况。ThreadPool.QueueUserWorkItem

When starting threads from the thread pool using ThreadPool.QueueUserWorkItem, however, you’ll possibly get an existing thread from the pool or a newly created thread, depending on what’s in the thread pool. Even if you aren’t creating threads yourself, the framework might be (as we’ve discussed regarding, for example, ASP.NET Core). This means that while some threads have a lifetime that’s rather short, others live for the duration of the entire application. Further complications arise when operations aren’t guaranteed to run on a single thread.

异步应用程序模型导致多线程问题

Asynchronous application models cause multi-threading issues

现代应用程序框架本质上是异步的。即使您的代码可能不使用asyncandawait关键字实现新的异步编程模式,您使用的框架可能仍会决定在与开始时不同的线程上完成请求。例如,ASP.NET Core 完全围绕这种异步编程模型构建。但更老ASP.NET Web API 和 ASP.NET Web Forms 等框架允许异步运行请求。

Modern application frameworks are inherently asynchronous in nature. Even though your code might not implement the new asynchronous programming patterns using the async and await keywords, the framework you’re using might still decide to finish a request on a different thread than it was started on. ASP.NET Core is, for instance, completely built around this asynchronous programming model. But even older frameworks, such as ASP.NET Web API and ASP.NET Web Forms, allow requests to run asynchronously.

这是绑定到特定线程的依赖项的问题。当请求在不同的线程上继续时,它仍然引用相同的Dependencies,即使其中一些依赖于原始线程。图 8.12说明了这一点。

This is a problem for Dependencies that are tied to a particular thread. When a request continues on a different thread, it still references the same Dependencies, even though some of them are tied to the original thread. Figure 8.12 illustrates this.

08-12.eps

图 8.12 特定于线程的依赖关系会导致异步环境中的并发错误。

Figure 8.12 Thread-specific Dependencies can cause concurrency bugs in asynchronous environments.

在异步上下文中运行时使用特定于线程的依赖项是一个特别糟糕的主意,因为它可能导致并发问题,而这些问题通常很难发现和重现。只有当特定于线程的依赖项不是线程安全的时才会出现这样的问题——它们通常不是。否则,单身人士生活方式会运作得很好。

Using thread-specific Dependencies while running in an asynchronous context is a particularly bad idea, because it could lead to concurrency problems, which are typically hard to find and reproduce. Such a problem would only occur if the thread-specific Dependency isn’t thread-safe — they typically aren’t. Otherwise, the Singleton Lifestyle would have worked just fine.

这个问题的解决方案是围绕请求或操作确定范围,有几种方法可以实现这一点。与其将依赖项的生命周期链接到线程的生命周期,不如将其生命周期限制在请求范围内,如第 8.3.3 节中所述。下面的清单再次证明了这一点。

The solution to this problem is to scope things around a request or operation, and there are several ways to achieve this. Instead of linking the lifetime of the Dependency to that of a thread, make its lifetime scoped to the request, as discussed in section 8.3.3. The following listing demonstrates this once more.

好的.tif

清单 8.24 存储作用域依赖在局部变量中

Listing 8.24 Storing Scoped Dependencies in local variables

static CurrencyParser CreateCurrencyParser(
    string connectionString)
{
    var context = new CommerceContext(    ①  
        connectionString);    ①  

    return new CurrencyParser(    ②  
        new SqlExchangeRateProvider(context),    ②  
        context);    ②  
}

本章中考察的生活方式代表了最常见的类型,但您可能有更多未得到满意解决的奇异需求。当我们发现自己处于这种情况时,我们的第一反应应该是意识到我们的方法一定是错误的,如果我们稍微改变一下设计,一切都会很好地适应标准模式。

The Lifestyles examined in this chapter represent the most common types, but you may have more exotic needs that aren’t satisfactorily addressed. When we find ourselves in such a situation, our immediate response should be to realize that our approach must be wrong, and if we change our design a bit, everything will fit nicely into standard patterns.

这种认识常常令人失望,但它会带来更好、更易于维护的代码。关键是,如果您觉得需要实现自定义Lifestyle或创建Leaky Abstraction,您应该首先认真地重新考虑您的设计。出于这个原因,我们决定将专门的生活方式排除在本书之外。我们通常可以通过重新设计或拦截来更好地处理这种情况,正如您将在下一章中看到的那样。

This realization is often a disappointment, but it leads to better and more maintainable code. The point is that if you feel the need to implement a custom Lifestyle or create a Leaky Abstraction, you should first seriously reconsider your design. For this reason, we decided to leave specialized Lifestyles out of this book. We can often handle such situations better with a redesign or Interception, as you’ll see in the next chapter.

概括

Summary

  • Composer是一个统一的术语,指的是组成Dependencies的任何对象或方法。它是Composition Root的重要组成部分。
  • Composer is a unifying term, referring to any object or method that composes Dependencies. It’s an important part of the Composition Root.
  • Composer可以是一个DI Container,但它也可以是任何使用Pure DI手动构建对象图的方法。
  • The Composer can be a DI Container, but it can also be any method that constructs object graphs manually using Pure DI.
  • ComposerDependencies生命周期的影响比任何单个消费者都大。Composer决定何时创建实例,并且通过选择是否共享实例,它确定依赖项是否超出单个消费者的范围,或者是否所有消费者都必须超出范围才能释放依赖项。
  • The Composer has a greater degree of influence over the lifetime of Dependencies than any single consumer can have. The Composer decides when instances are created, and, by its choice of whether to share instances, it determines whether a Dependency goes out of scope with a single consumer or whether all consumers must go out of scope before the Dependency can be released.
  • Lifestyle是描述Dependency预期生命周期的形式化方式。
  • A Lifestyle is a formalized way of describing the intended lifetime of a Dependency.
  • 微调每个DependencyLifestyle的能力对于性能原因很重要,但对于正确的行为也很重要。一些依赖关系必须在多个消费者之间共享,系统才能正常工作。
  • The ability to fine tune each Dependency’s Lifestyle is important for performance reasons but can also be important for correct behavior. Some Dependencies must be shared between several consumers for the system to work correctly.
  • Liskov 替换原则指出,您必须能够在不改变系统正确性的情况下用抽象替换任意实现。
  • The Liskov Substitution Principle states that you must be able to substitute the Abstraction for an arbitrary implementation without changing the correctness of the system.
  • 不遵守Liskov 替换原则会使应用程序变得脆弱,因为它不允许替换可能导致消费者中断的依赖项。
  • Failing to adhere to the Liskov Substitution Principle makes applications fragile, because it disallows replacing Dependencies that might cause a consumer to break.
  • 短暂的一次性对象是具有明确且短生命周期的对象,通常不会超过单个方法调用。
  • An ephemeral disposable is an object with a clear and short lifetime that typically doesn’t exceed a single method call.
  • 努力实现服务,这样它们就不会保留对一次性用品的引用,而是按需创建和处置它们。这使得内存管理更简单,因为服务可以像其他对象一样被垃圾回收。
  • Diligently work to implement services so they don’t hold references to disposables, but rather create and dispose of them on demand. This makes memory management simpler, because the service can be garbage collected like other objects.
  • 处理依赖项的责任落在Composer身上。它比其他任何东西都知道它何时创建一次性实例,因此它也知道该实例需要被处置。
  • The responsibility of disposing of Dependencies falls to the Composer. It, better than anything else, knows when it creates a disposable instance, so it also knows that the instance needs to be disposed of.
  • 释放是确定哪些依赖项可以取消引用和(可能)处置的过程。Composition RootComposer发出信号以释放已解决的Dependency
  • Releasing is the process of determining which Dependencies can be dereferenced and (possibly) disposed of. The Composition Root signals the Composer to release a resolved Dependency.
  • Composer必须注意对象的正确处置顺序。对象可能需要在处置期间调用其依赖项,如果这些依赖项已被处置,则会导致问题。因此,处置应该按照与对象创建相反的顺序进行。
  • A Composer must take care of the correct order of disposal for objects. An object might require its Dependencies to be called during disposal, which causes problems if these Dependencies are already disposed of. Disposal should, therefore, happen in the opposite order of object creation.
  • Transient Lifestyle涉及在每次请求时返回一个新实例。每个消费者都有自己的Dependency实例。
  • The Transient Lifestyle involves returning a new instance every time it’s requested. Each consumer gets its own instance of the Dependency.
  • 在单个Composer的范围内,只有一个具有Singleton Lifestyle的组件实例。每次消费者请求该组件时,都会提供相同的实例。
  • Within the scope of a single Composer, there’ll only be one instance of a component with the Singleton Lifestyle. Each time a consumer requests the component, the same instance is served.
  • Scoped Dependencies在一个明确定义的范围或请求中表现得像单例,但不跨范围共享。每个作用域都有自己的一组关联的Dependencies
  • Scoped Dependencies behave like singletons within a single, well-defined scope or request, but aren’t shared across scopes. Each scope has its own set of associated Dependencies.
  • Scoped Lifestyle对于长期运行的应用程序很有意义,这些应用程序的任务是处理需要在某种程度上隔离运行的操作。当并行处理这些操作时,或者当每个操作都包含自己的状态时,就需要隔离。
  • The Scoped Lifestyle makes sense for long-running applications that are tasked with processing operations that need to run in some degree of isolation. Isolation is required when these operations are processed in parallel, or when each operation contains its own state.
  • 如果您需要DbContext在 Web 请求中编写 Entity Framework Core,Scoped Lifestyle是一个很好的选择。DbContext实例不是线程安全的,但通常DbContext每个 Web 请求只需要一个实例。
  • If you ever need to compose an Entity Framework Core DbContext in a web request, a Scoped Lifestyle is an excellent choice. DbContext instances aren’t thread-safe, but you typically only want one DbContext instance per web request.
  • 对象图可以由不同Lifestyles的Dependencies组成,但您应该确保消费者只有生命周期等于或超过其自身的 Dependencies,因为消费者将保持其Dependencies存活。不这样做会导致Captive Dependencies
  • Object graphs can consist of Dependencies of different Lifestyles, but you should make sure that a consumer only has Dependencies with a lifetime that’s equal to or exceeds its own, because a consumer will keep its Dependencies alive. Failing to do so leads to Captive Dependencies.
  • Captive Dependency一种无意中保持存活时间过长的依赖关系,因为其消费者的生命周期超过了Dependency的预期生命周期。
  • A Captive Dependency is a Dependency that’s inadvertently kept alive for too long, because its consumer was given a lifetime that exceeds the Dependency’s expected lifetime.
  • 使用DI 容器时,俘虏依赖项是错误的常见来源,尽管使用Pure DI时也会出现此问题。
  • Captive Dependencies are a common source of bugs when working with a DI Container, although the problem can also arise when working with Pure DI.
  • 在应用Pure DI时,仔细构造Composition Root可以减少遇到问题的机会。
  • When applying Pure DI, a careful structure of the Composition Root can reduce the chance of running into problems.
  • 使用DI Container时,Captive Dependencies是一个普遍存在的问题,以至于一些DI Container对构造的对象图进行分析以检测它们。
  • When working with a DI Container, Captive Dependencies are such a widespread problem that some DI Containers perform analysis on constructed object graphs to detect them.
  • 有时您需要推迟Dependency的创建。但是,将依赖项作为Lazy<T>Func<T>或注入IEnumerable<T>是一个坏主意,因为它会导致依赖项变成泄漏抽象。相反,您应该将此知识隐藏在代理或组合后面。
  • Sometimes you need to postpone the creation of a Dependency. Injecting the Dependency as a Lazy<T>, Func<T>, or IEnumerable<T>, however, is a bad idea because it causes the Dependency to become a Leaky Abstraction. Instead, you should hide this knowledge behind a Proxy or Composite.
  • 不要将依赖项的生命周期绑定到线程的生命周期。线程的生命周期通常是不清楚的,在异步框架中使用它会导致多线程问题。相反,使用适当的Scoped Lifestyle或隐藏对代理背后的线程静态值的访问。
  • Don’t bind the lifetime of a Dependency to the lifetime of a thread. The lifetime of a thread is often unclear, and using it in an asynchronous framework can cause multi-threading issues. Instead, use a proper Scoped Lifestyle or hide access to the thread-static value behind a Proxy.

9

拦截

9

Interception

在这一章当中

In this chapter

  • 拦截两个协作对象之间的调用
  • Intercepting calls between two collaborating objects
  • 理解装饰器设计模式
  • Understanding the Decorator design pattern
  • 使用装饰器应用横切关注点
  • Applying Cross-Cutting Concerns using Decorators

烹饪最有趣的事情之一是您可以将许多成分(其中一些本身并不特别美味)组合成一个大于其部分之和的整体。通常,您从提供餐点基础的简单成分开始,然后对其进行修改和修饰,直到最终结果是一道美味的菜肴。

One of the most interesting things about cooking is the way you can combine many ingredients, some of them not particularly savory in themselves, into a whole that’s greater than the sum of its parts. Often, you start with a simple ingredient that provides the basis for the meal, and then modify and embellish it until the end result is a delicious dish.

考虑一份小牛肉排。如果你不顾一切,你可以生吃,但在大多数情况下你更喜欢油炸。但如果你把它放在热锅上,结果就不那么好了。除了烧焦的味道外,它的味道不会太大。幸运的是,您可以采取许多步骤来增强体验:

Consider a veal cutlet. If you were desperate, you could eat it raw, but in most cases you’d prefer to fry it. But if you slap it on a hot pan, the result will be less than stellar. Apart from the burned flavor, it won’t taste like much. Fortunately, there are lots of steps you can take to enhance the experience:

  • 用黄油煎炸肉排可以防止肉烧焦,但味道可能会保持平淡。
  • Frying the cutlet in butter prevents burning the meat, but the taste is likely to remain bland.
  • 加盐可以增强肉的味道。
  • Adding salt enhances the taste of the meat.
  • 添加其他香料,例如胡椒粉,会使味道更加复杂。
  • Adding other spices, such as pepper, makes the taste more complex.
  • 用包括盐和香料的混合物将其裹上面包屑不仅增加了味道,而且还使原始成分包裹在新的质地中。此时,您离拥有cotoletta越来越近了。1个 
  • Breading it with a mixture that includes salt and spices not only adds to the taste, but also envelops the original ingredient in a new texture. At this point, you’re getting close to having a cotoletta.1 
  • 在炸肉排上切开一个口袋,在裹上面包屑之前将火腿、奶酪和大蒜放入口袋中,这让我们超越了顶部。现在你有了蓝带小牛肉,一道非常棒的菜。
  • Slitting open a pocket in the cutlet and adding ham, cheese, and garlic into the pocket before breading it takes us over the top. Now you have veal cordon bleu, a most excellent dish.

烧小牛排和小牛肉蓝带之间的区别很明显,但基本成分是相同的。变化是由你添加的东西引起的。给小牛肉排,你可以在不改变主要成分的情况下对其进行修饰,从而创造出不同的菜肴。

The difference between a burned veal cutlet and veal cordon bleu is significant, but the basic ingredient is the same. The variation is caused by the things you add to it. Given a veal cutlet, you can embellish it without changing the main ingredient to create a different dish.

使用松散耦合,您可以在开发软件时执行类似的壮举。当您针对接口编程时,您可以通过将核心实现包装在该接口的其他实现中来转换或增强核心实现。您已经在清单 8.19 中看到了一些这种技术的实际应用,我们在其中使用这种技术通过将昂贵的Dependency包装在虚拟代理中来修改它的生命周期。2个 

With loose coupling, you can perform a similar feat when developing software. When you program to an interface, you can transform or enhance a core implementation by wrapping it in other implementations of that interface. You already saw a bit of this technique in action in listing 8.19, where we used this technique to modify an expensive Dependency’s lifetime by wrapping it in a Virtual Proxy.2 

这种方法可以推广,使您能够拦截消费者对服务的调用。这就是我们将在本章中介绍的内容。

This approach can be generalized, providing you with the ability to Intercept a call from a consumer to a service. This is what we’ll cover in this chapter.

就像小牛排一样,我们从一种基本成分开始,然后添加更多成分以使第一种成分变得更好,但不会改变它最初的核心。拦截是您从松散耦合中获得的最强大的能力之一。它使您能够轻松应用单一职责原则和关注点分离。

Like the veal cutlet, we start out with a basic ingredient and add more ingredients to make the first ingredient better, but without changing the core of what it was originally. Interception is one of the most powerful abilities that you gain from loose coupling. It enables you to apply the Single Responsibility Principle and Separation of Concerns with ease.

在前面的章节中,我们耗费了大量精力操纵代码,使其真正处于松耦合状态。在本章中,我们将开始收获这项投资的收益。本章的整体结构非常线性。我们将从Interception的介绍开始,包括一个示例。从那里开始,我们将继续讨论Cross-Cutting Concerns。本章轻理论重示例,因此如果您已经熟悉该主题,可以考虑直接转到第 10 章,该章讨论面向方面的编程

In the previous chapters, we expended a lot of energy maneuvering code into a position where it’s truly loosely coupled. In this chapter, we’ll start harvesting the benefits of that investment. The overall structure of this chapter is pretty linear. We’ll start with an introduction to Interception, including an example. From there, we’ll move on to talk about Cross-Cutting Concerns. This chapter is light on theory and heavy on examples, so if you’re already familiar with this subject, you can consider moving directly to chapter 10, which discusses Aspect-Oriented Programming.

完成本章后,您应该能够使用Interception和 Decorator 设计模式来开发松散耦合的代码。您应该获得成功观察关注点分离和应用横切关注点的能力,同时保持您的代码处于良好状态。

When you’re done with this chapter, you should be able to use Interception to develop loosely coupled code using the Decorator design pattern. You should gain the ability to successfully observe Separation of Concerns and apply Cross-Cutting Concerns, all while keeping your code in good condition.

本章从一个基本的介绍性示例开始,逐步构建越来越复杂的概念和示例。最后一个也是最先进的概念可以在摘要中快速解释。但是,因为它可能只有在一个可靠的例子中才有意义,所以本章以一个全面的、多页的演示来结束它是如何工作的。然而,在我们到达那一点之前,我们必须从头开始,即介绍拦截

This chapter starts with a basic, introductory example, building toward increasingly complex notions and examples. The final, and most advanced, concept can be quickly explained in the abstract. But, because it’ll probably only make sense with a solid example, the chapter culminates with a comprehensive, multipage demonstration of how it works. Before we get to that point, however, we must start at the beginning, which is to introduce Interception.

9.1 介绍拦截

9.1 Introducing Interception

拦截的概念很简单:我们希望能够拦截消费者和服务之间的调用,并在服务调用之前或之后执行一些代码。我们希望这样做的方式是消费者和服务都不必改变。

The concept of Interception is simple: we want to be able to intercept the call between a consumer and a service, and to execute some code before or after the service is invoked. And we want to do so in such a way that neither the consumer nor the service has to change.

例如,假设您想为一个SqlProductRepository类添加安全检查. 尽管您可以通过更改SqlProductRepository自身或通过更改消费者的代码来做到这一点,但使用InterceptionSqlProductRepository时,您可以通过拦截对使用某些中间代码段的调用来应用安全检查。在图 9.1中,消费者对服务的正常调用被中间人拦截,中间人可以在将调用传递给实际服务之前或之后执行自己的代码。

For example, imagine you want to add security checks to a SqlProductRepository class. Although you could do this by changing SqlProductRepository itself or by changing a consumer’s code, with Interception, you apply security checks by intercepting calls to SqlProductRepository using some intermediary piece of code. In figure 9.1, a normal call from a consumer to a service is intercepted by an intermediary that can execute its own code before or after passing the call to the real service.

09-01.eps

图 9.1 拦截概述

Figure 9.1 Interception in a nutshell

在本节中,您将熟悉拦截并了解其核心是如何应用装饰器设计模式的。如果您对 Decorator 模式的了解有点生疏,请不要担心;作为讨论的一部分,我们将从对该模式的描述开始。完成后,您应该对装饰器的工作原理有一个很好的理解。我们将首先看一个展示该模式的简单示例,然后讨论拦截与装饰器模式的关系。

In this section, you’re going to get acquainted with Interception and learn how, at its core, it’s an application of the Decorator design pattern. Don’t worry if your knowledge of the Decorator pattern is a bit rusty; we’ll start with a description of this pattern as part of the discussion. When we’re done, you should have a good understanding of how Decorators work. We’ll begin by looking at a simple example that showcases the pattern, and follow up with a discussion of how Interception relates to the Decorator pattern.

9.1.1 装饰器设计模式

9.1.1 Decorator design pattern

与许多其他模式一样,装饰器模式是一种古老且描述良好的设计模式,它比 DI 早十年。它是Interception的一个基本部分,值得回顾一下。

As is the case with many other patterns, the Decorator pattern is an old and well-described design pattern that predates DI by a decade. It’s such a fundamental part of Interception that it warrants a refresher.

Erich Gamma 等人在设计模式:可重用面向对象软件的元素一书中首次描述了装饰器模式。(Addison-Wesley, 1994)。该模式的目的是“动态地将附加职责附加到对象。装饰器为扩展功能提供了一种灵活的子类化替代方案。” 3个 

The Decorator pattern was first described in the book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. (Addison-Wesley, 1994). The pattern’s intent is to “attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.”3 

如图9.2所示,装饰器通过将抽象的一个实现包装在同一抽象另一个实现中来工作。此包装器将操作委托给包含的实现,同时在调用包装对象之前和/或之后添加行为。

As figure 9.2 shows, a Decorator works by wrapping one implementation of an Abstraction in another implementation of the same Abstraction. This wrapper delegates operations to the contained implementation, while adding behavior before and/or after invoking the wrapped object.

09-02.eps

图 9.2 装饰者模式的一般结构

Figure 9.2 General structure of the Decorator pattern

动态附加职责的能力意味着您可以决定在运行时应用装饰器,而不是在编译时将这种关系嵌入到程序中,而这正是您对子类化所做的。

The ability to attach responsibilities dynamically means that you can make the decision to apply a Decorator at runtime rather than having this relationship baked into the program at compile time, which is what you’d do with subclassing.

一个装饰器可以包装另一个装饰器,另一个装饰器包装另一个装饰器,依此类推,提供拦截的“管道”。图 9.3显示了这是如何工作的。在核心,必须有一个独立的实现来执行所需的工作。

A Decorator can wrap another Decorator, which wraps another Decorator, and so on, providing a “pipeline” of interception. Figure 9.3 shows how this works. At the core, there must be a self-contained implementation that performs the desired work.

09-03.eps

图 9.3 就像一组俄罗斯套娃一样,一个 Decorator 包裹另一个 Decorator,后者包裹一个独立的组件。4个 

Figure 9.3 Like a set of Russian nesting dolls, a Decorator wraps another Decorator that wraps a self-contained component.4 

例如,假设您有一个名为Abstraction的方法,其中包含一个方法IGreeterGreet:

Let’s say, for instance, that you have an Abstraction called IGreeter that contains a Greet method:

public interface IGreeter
{
    string Greet(string name);
}

对于这个抽象,您可以创建一个简单的实现来创建正式的问候语:

For this Abstraction, you can create a simple implementation that creates a formal greeting:

public class FormalGreeter : IGreeter
{
    public string Greet(string name)
    {
        return "Hello, " + name + ".";
    }
}

最简单的 Decorator 实现是在不执行任何操作的情况下委托对装饰对象的调用:

The simplest Decorator implementation is one that delegates the call to the decorated object without doing anything at all:

public class SimpleDecorator : IGreeter    ①  
{
    private readonly IGreeter decoratee;    ①  

    public SimpleDecorator(IGreeter decoratee)
    {
        this.decoratee = decoratee;
    }

    public string Greet(string name)
    {
        return this.decoratee.Greet(name);    ②  
    }
}

图 9.4IGreeter显示了、和之间的关系。因为除了转发呼叫外什么都不做,所以它没什么用。相反,装饰器可以选择在委托调用之前修改输入。FormalGreeterSimpleDecoratorSimpleDecorator

Figure 9.4 shows the relationship between IGreeter, FormalGreeter, and SimpleDecorator. Because SimpleDecorator doesn’t do anything except forward the call, it’s pretty useless. Instead, a Decorator can choose to modify the input before delegating the call.

09-04.eps

图 9.4 BothSimpleDecoratorFormalGreeterimplement IGreeter, whileSimpleDecorator包装 an并将来自其方法的IGreeter任何调用转发给被装饰者的方法。GreetGreet

Figure 9.4 Both SimpleDecorator and FormalGreeter implement IGreeter, while SimpleDecorator wraps an IGreeter and forwards any calls from its Greet method to the Greet method of the decoratee.

我们先看Greet一个TitledGreeterDecorator类的方法:

Let’s take a look at the Greet method of a TitledGreeterDecorator class:

public string Greet(string name)
{
    string titledName = "Mr. " + name;
    return this.decoratee.Greet(titledName);
}

以类似的方式,装饰器可能会决定在创建返回值之前修改返回值NiceToMeetYouGreeterDecorator

In a similar move, the Decorator may decide to modify the return value before returning it when you create a NiceToMeetYouGreeterDecorator:

public string Greet(string name)
{
    string greet = this.decoratee.Greet(name);
    return greet + " Nice to meet you.";
}

鉴于前面的两个示例,您可以将后者包裹在前者的周围以组成一个同时修改输入和输出的组合:

Given the two previous examples, you can wrap the latter around the former to compose a combination that modifies both input and output:

IGreeter greeter =
    new NiceToMeetYouGreeterDecorator(
        new TitledGreeterDecorator(
            new FormalGreeter()));

string greet = greeter.Greet("Samuel L. Jackson");
Console.WriteLine(greet);

这会产生以下输出:

This produces the following output:

09-09_hedgehog.eps

装饰器也可以决定不调用底层实现:

A Decorator may also decide not to invoke the underlying implementation:

public string Greet(string name)
{
    if (name == null)    ①  
    {
        return "Hello world!";
    }

    return this.decoratee.Greet(name);
}

不调用底层实现比委托调用更重要。尽管跳过被装饰者本质上没有错,但装饰器现在取代了而不是丰富了原始行为。5  更常见的情况是通过抛出异常来停止执行,我们将在 9.2.3 节中讨论。

Not invoking the underlying implementation is more consequential than delegating the call. Although there’s nothing inherently wrong with skipping the decoratee, the Decorator now replaces, rather than enriches, the original behavior.5  A more common scenario is to stop execution by throwing an exception, as we’ll discuss in section 9.2.3.

Decorator 与任何包含Dependencies的类的区别在于,包装对象实现与 Decorator 相同的抽象。这使得Composer可以在不改变消费者的情况下用 Decorator 替换原始组件。包装对象通常被注入到声明为抽象类型的 Decorator 中——它包装了接口,而不是特定的具体实现。在这种情况下,Decorator 必须遵守Liskov 替换原则,平等对待所有被装饰的对象。

What differentiates a Decorator from any class containing Dependencies is that the wrapped object implements the same Abstraction as the Decorator. This enables a Composer to replace the original component with a Decorator without changing the consumer. The wrapped object is often injected into the Decorator declared as the abstract type — it wraps the interface, not a specific, concrete implementation. In that case, the Decorator must adhere to the Liskov Substitution Principle and treat all decorated objects equally.

而已。装饰器模式没有比这更多的了。您已经在本书的多个地方看到了装饰器的实际应用。例如,1.2.2 节中的示例是装饰器。现在让我们看一个具体的例子,说明我们如何使用 Decorator 来实现Cross-Cutting ConcernSecureMessageWriter

That’s it. There isn’t much more to the Decorator pattern than this. You’ve already seen Decorators in action several places in this book. The SecureMessageWriter example in section 1.2.2, for instance, is a Decorator. Now let’s look at a concrete example of how we can use a Decorator to implement a Cross-Cutting Concern.

9.1.2 示例:使用装饰器实现审计

9.1.2 Example: Implementing auditing using a Decorator

在这个例子中,我们将IUserRepository再次实施审计。您可能还记得,我们在 6.3 节中讨论了审计,我们在解释如何修复依赖循环时以它为例。通过审计,您可以记录用户在系统中进行的所有重要操作以供以后分析。

In this example, we’ll implement auditing for the IUserRepository again. As you might recall, we discussed auditing in section 6.3, where we used it as an example when explaining how to fix Dependency cycles. With auditing, you record all of the important actions users make in a system for later analysis.

审计是横切关注点的一个常见示例:它可能是必需的,但阅读和编辑用户的核心功能不应受到审计的影响。这正是我们在 6.3 节中所做的。因为我们注入了IAuditTrailAppender接口进入SqlUserRepository自身,我们强迫它了解并实施审计。这是违反单一职责原则的。单一职责原则建议我们不应该让实施SqlUserRepository审计;鉴于此,使用装饰器是更好的选择。

Auditing is a common example of a Cross-Cutting Concern: it may be required, but the core functionality of reading and editing users shouldn’t be affected by auditing. This is exactly what we did in section 6.3. Because we injected the IAuditTrailAppender interface into the SqlUserRepository itself, we forced it to know about and to implement auditing. This is a Single Responsibility Principle violation. The Single Responsibility Principle suggests that we shouldn’t let SqlUserRepository implement auditing; given this, using a Decorator is a better alternative.

为用户存储库实现审计装饰器

Implementing an auditing Decorator for the user repository

AuditingUserRepositoryDecorator您可以通过引入一个新类来使用装饰器实现审计包装另一个并实施审计IUserRepository. 图 9.5说明了类型之间的关系。

You can implement auditing with a Decorator by introducing a new AuditingUserRepositoryDecorator class that wraps another IUserRepository and implements auditing. Figure 9.5 illustrates how the types relate to each other.

09-05.eps

图 9.5 AuditingUserRepositoryDecorator将审计添加到任何IUserRepository实现中。

Figure 9.5 AuditingUserRepositoryDecorator adds auditing to any IUserRepository implementation.

除了一个装饰器,还需要一个实现审计的服务。为此,您可以使用第 6.3 节中的内容。下面的清单显示了这个实现。IUserRepositoryAuditingUserRepositoryDecoratorIAuditTrailAppender

In addition to a decorated IUserRepository, AuditingUserRepositoryDecorator also needs a service that implements auditing. For this, you can use IAuditTrailAppender from section 6.3. The following listing shows this implementation.

清单 9.1 声明一个AuditingUserRepositoryDecorator

Listing 9.1 Declaring an AuditingUserRepositoryDecorator

public class AuditingUserRepositoryDecorator
    : IUserRepository    ①  
{
    private readonly IAuditTrailAppender appender;
    private readonly IUserRepository decoratee;

    public AuditingProductRepository(
        IAuditTrailAppender appender,
        IUserRepository decoratee)    ①  
    {
        this.appender = appender;
        this.decoratee = decoratee;
    }

    ...
}

AuditingUserRepositoryDecorator实现了它所装饰的相同抽象。它使用标准的构造函数注入来请求IUserRepository它可以包装并可以将其核心实现委托给的对象。除了装饰的存储库之外,它还请求一个IAuditTrailAppender它可以用来审计由装饰的存储库实现的操作。以下清单显示了 上两种方法的示例实现AuditingUserRepositoryDecorator

AuditingUserRepositoryDecorator implements the same Abstraction that it decorates. It uses standard Constructor Injection to request an IUserRepository that it can wrap and to which it can delegate its core implementation. In addition to the decorated Repository, it also requests an IAuditTrailAppender it can use to audit the operations implemented by the decorated Repository. The following listing shows sample implementations of two methods on AuditingUserRepositoryDecorator.

清单 9.2 实现AuditingUserRepositoryDecorator

Listing 9.2 Implementing AuditingUserRepositoryDecorator

public User GetById(Guid id)    ①  
{    ①  
    return this.decoratee.GetById(id);    ①  
}    ①  

public void Update(User user)    ②  
{    ②  
    this.decoratee.Update(user);    ②  
    this.appender.Append(user);    ②  
}    ②  

并非所有操作都需要审计。一个常见的要求是审核所有创建、更新和删除操作,同时忽略读取操作。因为GetById方法是纯读取操作,您将调用委托给装饰过的 Repository 并立即返回结果。Update另一方面,必须审核该方法。您仍然将实现委托给装饰后的 Repository,但是在委托方法成功返回后,您使用注入IAuditTrailAppender来审计操作。

Not all operations need auditing. A common requirement is to audit all create, update, and delete operations, while ignoring read operations. Because the GetById method is a pure read operation, you delegate the call to the decorated Repository and immediately return the result. The Update method, on the other hand, must be audited. You still delegate the implementation to the decorated Repository, but after the delegated method returns successfully, you use the injected IAuditTrailAppender to audit the operation.

装饰器,如,类似于小牛肉排周围的面包屑:它在不修改基本成分的情况下修饰它。面包屑本身不是空壳,而是有自己的配料表。真正的面包屑是由面包屑和香料制成的;同样,包含一个.AuditingUserRepositoryDecoratorAuditingUserRepositoryDecoratorIAuditTrailAppender

A Decorator, like AuditingUserRepositoryDecorator, is similar to the breading around the veal cutlet: it embellishes the basic ingredient without modifying it. The breading itself isn’t an empty shell, but comes with its own list of ingredients. Real breading is made from breadcrumbs and spices; similarly, AuditingUserRepositoryDecorator contains an IAuditTrailAppender.

请注意,注入IAuditTrailAppender本身就是一个抽象,这意味着您可以独立于AuditingUserRepositoryDecorator. AuditingUserRepositoryDecorator全班_做的是协调装饰器IUserRepository和的动作IAuditTrailAppender。您可以编写您喜欢的任何实现IAuditTrailAppender,但在清单 6.24 中,我们选择构建一个基于实体框架的实现。让我们看看如何连接所有相关的依赖关系来完成这项工作。

Note that the injected IAuditTrailAppender is itself an Abstraction, which means that you can vary the implementation independently of AuditingUserRepositoryDecorator. All the AuditingUserRepositoryDecorator class does is coordinate the actions of the decorated IUserRepository and IAuditTrailAppender. You can write any implementation of IAuditTrailAppender you like, but in listing 6.24, we chose to build one based on the Entity Framework. Let’s see how you can wire up all relevant Dependencies to make this work.

构成AuditingUserRepositoryDecorator

Composing AuditingUserRepositoryDecorator

在第 8 章中,您看到了几个如何组合HomeController实例的示例。清单 8.11 提供了一个关于具有Transient Lifestyle实例的简单实现。以下清单显示了如何HomeController使用装饰的SqlUserRepository.

In chapter 8, you saw several examples of how to compose a HomeController instance. Listing 8.11 provided a simple implementation concerning instances with a Transient Lifestyle. The following listing shows how you can compose this HomeController using a decorated SqlUserRepository.

清单 9.3 组合装饰器

Listing 9.3 Composing a Decorator

private HomeController CreateHomeController()
{
    var context = new CommerceContext();

    IAuditTrailAppender appender =
        new SqlAuditTrailAppender(
            this.userContext,
            context);

    IUserRepository userRepository =    ①  
        new AuditingUserRepositoryDecorator(    ①  
            appender,    ①  
            new SqlUserRepository(context));    ①  

    IProductService productService =
        new ProductService(
            new SqlProductRepository(context),
            this.userContext,
            userRepository);    ②  

    return new HomeController(productService);
}

IUserRepository请注意,您可以在不更改现有类的源代码的情况下添加行为。您无需更改SqlUserRepository即可添加审核。回忆一下 4.4.2 节,这是一个被称为开放/封闭原则的理想特征。

Notice that you were able to add behavior to IUserRepository without changing the source code of existing classes. You didn’t have to change SqlUserRepository to add auditing. Recall from section 4.4.2 that this is a desirable trait known as the Open/Closed Principle.

既然您已经看到了SqlUserRepository使用装饰拦截具体的示例AuditingUserRepositoryDecorator,让我们将注意力转向在面对不一致或不断变化的需求时编写干净且可维护的代码,并解决横切关注点

Now that you’ve seen an example of intercepting the concrete SqlUserRepository with a decorating AuditingUserRepositoryDecorator, let’s turn our attention to writing clean and maintainable code in the face of inconsistent or changing requirements, and to addressing Cross-Cutting Concerns.

9.2 实施横切关注点

9.2 Implementing Cross-Cutting Concerns

大多数应用程序必须解决不直接与任何特定功能相关的方面,而是解决更广泛的问题。这些担忧往往会触及许多其他方面不相关的代码区域,即使在不同的模块或层中。因为它们跨越代码库的广泛区域,所以我们称它们为Cross-Cutting Concerns表 9.1列出了一些示例。该表不是一个完整的列表;相反,它是一个说明性的样本。

Most applications must address aspects that don’t directly relate to any particular feature, but, rather, address a wider matter. These concerns tend to touch many otherwise unrelated areas of code, even in different modules or layers. Because they cut across a wide area of the code base, we call them Cross-Cutting Concerns. Table 9.1 lists some examples. This table isn’t a comprehensive listing; rather, it’s an illustrative sampling.

表 9.1交叉关注点 的常见示例
方面描述
审计任何数据更改操作都应留下审计线索,包括时间戳、执行更改的用户的身份以及有关更改内容的信息。您在第 9.1.2 节中看到了这样的示例。
记录与审计略有不同,日志记录往往侧重于记录反映应用程序状态的事件。这可能是 IT 操作人员感兴趣的事件,但也可能是业务事件。
性能监控与日志记录略有不同,因为它更多地处理记录性能而不是特定事件。如果您的服务水平协议 (SLA) 无法通过标准基础架构进行监控,则您必须实施自己的性能监控。自定义 Windows 性能计数器是一个不错的选择,但您仍然必须添加一些代码来捕获数据。
验证通常需要使用有效数据调用操作。这可以是简单的用户输入验证或更复杂的业务规则验证。尽管验证本身总是依赖于它的上下文,但是验证的调用和验证结果的处理通常不是并且可以被认为是横切的。
安全某些操作只允许某些用户使用,通常基于角色或组的成员身份,您必须强制执行此操作。
缓存您通常可以通过实施缓存来提高性能,但是没有理由让特定的数据访问组件来处理这个方面。您可能希望能够为不同的数据访问实现启用或禁用缓存。
错误处理应用程序可能需要处理某些异常并记录它们、转换它们或向用户显示消息。您可以使用错误处理装饰器以适当的方式处理错误。
容错性进程外资源保证时不时不可用。关系数据库需要处理事务性操作以防止可能导致死锁的数据损坏。使用装饰器,您可以实现容错模式(例如断路器)来解决此问题。
09-06.eps

图 9.6 在应用程序架构图中,横切关注点通常由跨越所有层的垂直块表示。在这种情况下,安全是一个Cross-Cutting Concern

Figure 9.6 In application architecture diagrams, Cross-Cutting Concerns are typically represented by vertical blocks that span all layers. In this case, security is a Cross-Cutting Concern.

当您绘制分层应用程序架构图时,横切关注点通常表示为放置在层旁边的垂直块。如图 9.6所示。

When you draw diagrams of layered application architecture, Cross-Cutting Concerns are often represented as vertical blocks placed beside the layers. This is shown in figure 9.6.

在本节中,我们将看一些示例,这些示例说明如何以装饰器的形式使用拦截来解决Cross-Cutting Concerns。从表 9.1中,我们将选择容错、错误处理和安全方面来感受实现方面。与许多其他概念一样,拦截在抽象上很容易理解,但细节却很麻烦。需要接触才能正确吸收该技术,这就是本节显示三个示例的原因。当我们完成这些后,您应该更清楚地了解什么是拦截是以及如何应用它。因为您已经在第 9.1.2 节中看到了一个介绍性示例,所以我们将看一个更复杂的示例来说明如何将拦截与任意复杂的逻辑一起使用。

In this section, we’ll look at some examples that illustrate how to use Interception in the form of Decorators to address Cross-Cutting Concerns. From table 9.1, we’ll pick the fault tolerance, error handling, and security aspects to get a feel for implementing aspects. As is the case with many other concepts, Interception can be easy to understand in the abstract, but the devil is in the details. It takes exposure to properly absorb the technique, and that’s why this section shows three examples. When we’re done with these, you should have a clearer picture of what Interception is and how you can apply it. Because you already saw an introductory example in section 9.1.2, we’ll take a look at a more complex example to illustrate how Interception can be used with arbitrarily complex logic.

9.2.1 用断路器拦截

9.2.1 Intercepting with a Circuit Breaker

任何与进程外资源通信的应用程序偶尔会发现该资源不可用。网络连接中断,数据库离线,Web 服务被分布式拒绝服务淹没(DDOS)攻击。在这种情况下,调用应用程序必须能够恢复并适当地处理问题。

Any application that communicates with an out-of-process resource will occasionally find that the resource is unavailable. Network connections go down, databases go offline, and web services get swamped by Distributed Denial of Service (DDOS) attacks. In such cases, the calling application must be able to recover and appropriately deal with the issue.

大多数 .NET API 都有默认超时,以确保进程外调用不会永远阻塞使用线程。不过,在您收到超时异常的情况下,您如何处理对错误资源的下一次调用?您是否尝试再次调用该资源?因为超时通常表示另一端离线或被请求淹没,所以进行新的阻塞调用可能不是一个好主意。最好假设最坏的情况并立即抛出异常。这就是断路器模式背后的基本原理。

Most .NET APIs have default timeouts that ensure that an out-of-process call doesn’t block the consuming thread forever. Still, in a situation where you receive a timeout exception, how do you treat the next call to the faulting resource? Do you attempt to call the resource again? Because a timeout often indicates that the other end is either offline or swamped by requests, making a new blocking call may not be a good idea. It would be better to assume the worst and throw an exception immediately. This is the rationale behind the Circuit Breaker pattern.

断路器是一种稳定性模式,它通过快速失败而不是挂起并在挂起时消耗资源来增加应用程序的健壮性。这是非功能性需求和真正的Cross-Cutting Concern的一个很好的例子,因为它与进程外调用中实现的特性关系不大。

Circuit Breaker is a stability pattern that adds robustness to an application by failing fast instead of hanging and consuming resources as it hangs. This is a good example of a non-functional requirement and a true Cross-Cutting Concern, because it has little to do with the feature implemented in the out-of-process call.

断路器模式本身有点复杂,实施起来可能很复杂,但您只需进行一次投资。如果愿意,您甚至可以在可重用的库中实现它,您可以在其中通过使用装饰器模式轻松地将它应用于多个组件。

The Circuit Breaker pattern itself is a bit complex and can be intricate to implement, but you only need to make that investment once. You could even implement it in a reusable library if you liked, where you could easily apply it to multiple components by employing the Decorator pattern.

断路器模式

The Circuit Breaker pattern

断路器设计模式的名字来源于同名的电子开关。6  设计用于故障发生时切断连接,防止故障传播。

The Circuit Breaker design pattern takes its name from the electric switch of the same name.6  It’s designed to cut the connection when a fault occurs, preventing the fault from propagating.

在软件应用程序中,一旦发生超时或类似的通信错误,如果您继续敲击已关闭的系统,情况可能会变得更糟。如果远程系统被淹没,多次重试可以使它越过边缘——暂停可能会给它一个恢复的机会。在调用层,阻塞等待超时的线程会使消费应用程序无响应,迫使用户等待错误消息。最好在一段时间内快速检测到通信中断和失败。

In software applications, once a timeout or similar communications error occurs, it can make a bad situation worse if you keep hammering a downed system. If the remote system is swamped, multiple retries can take it over the edge — a pause might give it a chance to recover. On the calling tier, threads blocked waiting for timeouts can make the consuming application unresponsive, forcing a user to wait for an error message. It’s better to detect that communications are down and fail fast for a period of time.

断路器设计通过在发生错误时使开关跳闸来解决这个问题。它通常包括一个超时,使其稍后重试连接;这样,它可以在远程系统恢复时自动恢复。图 9.7说明了断路器中状态转换的简化视图。

The Circuit Breaker design addresses this by tripping the switch when an error occurs. It usually includes a timeout that makes it retry the connection later; this way, it can automatically recover when the remote system comes back up. Figure 9.7 illustrates a simplified view of the state transitions in a Circuit Breaker.

09-07.eps

图 9.7 断路器模式的简化状态转换图

Figure 9.7 Simplified state transition diagram of the Circuit Breaker pattern

您可能希望使断路器比图 9.7中描述的更复杂。首先,您可能不想在每次出现偶发错误时都跳闸,而是使用一个阈值。其次,你应该只在某些类型的错误上跳闸。超时和通信异常都很好,但很可能表示错误而不是间歇性错误。NullReferenceException

You may want to make a Circuit Breaker more complex than described in figure 9.7. First, you may not want to trip the breaker every time a sporadic error occurs but, rather, use a threshold. Second, you should only trip the breaker on certain types of errors. Timeouts and communication exceptions are fine, but a NullReferenceException is likely to indicate a bug instead of an intermittent error.

让我们看一个示例,该示例展示了如何使用装饰器模式将断路器行为添加到现有的进程外组件。在此示例中,我们将重点关注可重用断路器的应用,而不是其实现方式。

Let’s look at an example that shows how the Decorator pattern can be used to add Circuit Breaker behavior to an existing out-of-process component. In this example, we’ll focus on applying the reusable Circuit Breaker, but not on how it’s implemented.

示例:为创建断路器IProductRepository

Example: creating a Circuit Breaker for IProductRepository

IProductRepository在 7.2 节中,我们创建了一个 UWP 应用程序,它使用接口与后端数据源(例如 WCF 或 Web API 服务)进行通信. 在清单 8.6 中,我们使用了一个通过调用 WCF 服务操作来实现的。因为这个实现没有明确的错误处理,所以任何通信错误都会冒泡到调用者。WcfProductRepositoryIProductRepository

In section 7.2, we created a UWP application that communicates with a backend data source, such as a WCF or Web API service, using the IProductRepository interface. In listing 8.6, we used a WcfProductRepository that implements IProductRepository by invoking the WCF service operations. Because this implementation has no explicit error handling, any communication error will bubble up to the caller.

这是使用断路器的绝佳场景。一旦异常开始发生,您希望快速失败;这样,您就不会阻塞调用线程并淹没服务。正如下一个清单所示,您首先声明一个装饰器并通过构造函数注入IProductRepository请求必要的依赖项。

This is an excellent scenario in which to use a Circuit Breaker. You’d like to fail fast once exceptions start occurring; this way, you won’t block the calling thread and swamp the service. As the next listing shows, you start by declaring a Decorator for IProductRepository and requesting the necessary Dependencies via Constructor Injection.

清单 9.4 用断路器装饰

Listing 9.4 Decorating with a Circuit Breaker

public class CircuitBreakerProductRepositoryDecorator    ①  
    : IProductRepository    ①  
{
    private readonly ICircuitBreaker breaker;
    private readonly IProductRepository decoratee;    ①  

    public CircuitBreakerProductRepositoryDecorator(
        ICircuitBreaker breaker,    ②  
        IProductRepository decoratee)
    {
        this.breaker = breaker;
        this.decoratee = decoratee;
    }

    ...
}

您现在可以包装对装饰的任何调用IProductRepository

You can now wrap any call to the decorated IProductRepository.

清单 9.5 将断路器应用于该Insert方法

Listing 9.5 Applying a Circuit Breaker to the Insert method

public void Insert(Product product)
{
    this.breaker.Guard();    ①  

    try
    {
        this.decoratee.Insert(product);  ②  
        this.breaker.Succeed();
    }
    catch (Exception ex)    ③  
    {    ③  
        this.breaker.Trip(ex);    ③  
        throw;    ③  
    }
}

在调用装饰存储库之前,您需要做的第一件事是检查断路器的状态。该Guard方法允许您在状态为 Closed 或 Half-Open 时通过,而在状态为 Open 时抛出异常。当您有理由相信调用不会成功时,这可以确保您快速失败。如果你通过了Guard方法,您可以尝试调用装饰的存储库。如果呼叫失败,您将触发断路器。在此示例中,我们保持简单,但在正确的实现中,您应该只捕获并从选定的异常类型中触发断路器。

The first thing you need to do before you invoke the decorated Repository is check the state of the Circuit Breaker. The Guard method lets you through when the state is either Closed or Half-Open, whereas it throws an exception when the state is Open. This ensures that you fail fast when you have reason to believe that the call isn’t going to succeed. If you make it past the Guard method, you can attempt to invoke the decorated Repository. If the call fails, you trip the breaker. In this example, we’re keeping things simple, but in a proper implementation, you should only catch and trip the breaker from a selection of exception types.

从 Closed 和 Half-Open 状态,使断路器跳闸会使您回到 Open 状态. 从打开状态,超时决定何时返回半打开状态.

From both the Closed and Half-Open states, tripping the breaker puts you back in the Open state. From the Open state, a timeout determines when you move back to the Half-Open state.

相反,如果调用成功,您将向断路器发出信号。如果您已经处于 Closed 状态,您将保持关闭状态。如果您处于半开状态,则会转换回关闭状态。当断路器处于打开状态时,不可能发出成功信号,因为该Guard方法可确保您永远不会走那么远。

Conversely, you signal the Circuit Breaker if the call succeeds. If you’re already in the Closed state, you stay in the Closed state. If you’re in the Half-Open state, you transition back to Closed. It’s impossible to signal success when the Circuit Breaker is in the Open state, because the Guard method ensures that you never get that far.

所有其他方法IProductRepository看起来都相似,唯一的区别是它们在 上调用的decoratee方法以及返回值的方法的额外代码行。您可以在该方法的try块内看到这种变化GetAll:

All other methods of IProductRepository look similar, with the only difference being the method they invoke on the decoratee and an extra line of code for methods that return a value. You can see this variation inside the try block for the GetAll method:

var products = this.decoratee.GetAll();
this.breaker.Succeed();
return products;

因为你必须向断路器指示成功,所以你必须在返回之前保存装饰存储库的返回值。这是返回值的方法和不返回值的方法之间的唯一区别。

Because you must indicate success to the Circuit Breaker, you have to hold the return value of the decorated repository before returning it. That’s the only difference between methods that return a value and methods that don’t.

此时,您已经离开了开放的实现,但真正的实现是一个完全可重用的复合类,它采用了 State 设计模式。7  虽然我们不打算深入探讨此处的实现,但重要的信息是您可以使用任意复杂的代码进行拦截。ICircuitBreakerCircuitBreaker

At this point, you’ve left the implementation of ICircuitBreaker open, but the real implementation is a completely reusable complex of classes that employ the State design pattern.7  Although we aren’t going to dive deeper into the implementation of CircuitBreaker here, the important message is that you can Intercept with arbitrarily complex code.

使用断路器实现组合应用程序

Composing the application using the Circuit Breaker implementation

要编写一个IProductRepository添加了断路器功能的 ,您可以将装饰器包装在真正的实现周围:

To compose an IProductRepository with Circuit Breaker functionality added, you can wrap the Decorator around the real implementation:

var channelFactory = new ChannelFactory<IProductManagementService>("*");

var timeout = TimeSpan.FromMinutes(1);

ICircuitBreaker breaker = new CircuitBreaker(timeout);

IProductRepository repository =
    new CircuitBreakerProductRepositoryDecorator(    ①  
        breaker,
        new WcfProductRepository(channelFactory));

在清单 7.6 中,我们从几个Dependencies组成了一个 UWP 应用程序,包括WcfProductRepository清单 8.6 中的一个实例。您可以WcfProductRepository通过将其注入实例来对其进行装饰,因为它实现了相同的接口。在此示例中,每次解析Dependencies时都会创建该类的一个新实例。这对应于短暂的生活方式CircuitBreakerProductRepositoryDecoratorCircuitBreaker

In listing 7.6, we composed a UWP application from several Dependencies, including a WcfProductRepository instance in listing 8.6. You can decorate this WcfProductRepository by injecting it into a CircuitBreakerProductRepositoryDecorator instance, because it implements the same interface. In this example, you create a new instance of the CircuitBreaker class every time you resolve Dependencies. That corresponds to the Transient Lifestyle.

在 UWP 应用程序中,您只需解决一次依赖关系,使用瞬态断路器不是问题,但通常,这不是此类功能的最佳生活方式。另一端只有一个 Web 服务。如果此服务不可用,断路器应断开所有连接尝试。如果CircuitBreakerProductRepositoryDecorator使用了多个实例,则所有实例都应该发生这种情况。

In a UWP application, where you only resolve the Dependencies once, using a Transient Circuit Breaker isn’t an issue but, in general, this isn’t the optimal lifestyle for such functionality. There’ll only be a single web service at the other end. If this service becomes unavailable, the Circuit Breaker should disconnect all attempts to connect to it. If several instances of CircuitBreakerProductRepositoryDecorator are in use, this should happen for all of them.

设置SingletonCircuitBreaker生命周期的情况很明显,但这也意味着它必须是线程安全的。由于其性质,保持状态;必须显式实现线程安全。这使得实现更加复杂。CircuitBreaker

There’s an obvious case for setting up CircuitBreaker with the Singleton lifetime, but that also means that it must be thread-safe. Due to its nature, CircuitBreaker maintains state; thread-safety must be explicitly implemented. This makes the implementation even more complex.

尽管它很复杂,但您可以使用断路器轻松拦截实例。IProductRepository尽管第 9.1.2 节中的第一个拦截示例相当简单,但断路器示例演示了您可以拦截一个类具有跨领域关注点横切关注点很容易比原始实现更复杂。

Despite its complexity, you can easily Intercept an IProductRepository instance with a Circuit Breaker. Although the first Interception example in section 9.1.2 was fairly simple, the Circuit Breaker example demonstrates that you can intercept a class with a Cross-Cutting Concern. The Cross-Cutting Concern can easily be more complex than the original implementation.

断路器模式确保应用程序快速失败,而不是占用宝贵的资源。理想情况下,应用程序根本不会崩溃。要解决此问题,您可以使用Interception实现某些类型的错误处理。

The Circuit Breaker pattern ensures that an application fails fast instead of tying up precious resources. Ideally, the application wouldn’t crash at all. To address this issue, you can implement some kinds of error handling with Interception.

9.2.2 使用装饰器模式报告异常

9.2.2 Reporting exceptions using the Decorator pattern

依赖项可能会不时抛出异常。如果遇到无法处理的情况,即使是写得最好的代码也会(并且应该)抛出异常。消耗进程外资源的客户端属于这一类。示例 UWP 应用程序中的类就是一个示例。当 Web 服务不可用时,存储库将开始抛出异常。断路器不会改变这一基本特征。虽然它拦截了 WCF 客户端,但它仍然会抛出异常——它做得更快。WcfProductRepository

Dependencies are likely to throw exceptions from time to time. Even the best-written code will (and should) throw exceptions if it encounters situations it can’t deal with. Clients that consume out-of-process resources fall into that category. A class like WcfProductRepository from the sample UWP application is one example. When the web service is unavailable, the Repository will start throwing exceptions. A Circuit Breaker doesn’t change this fundamental trait. Although it Intercepts the WCF client, it still throws exceptions — it does so quicker.

您可以使用拦截来添加错误处理。您不想通过错误处理来增加依赖项的负担。因为Dependency应该被视为可以在许多不同场景中使用的可重用组件,所以不可能向Dependency添加适合所有场景的异常处理策略。如果这样做,也将违反单一职责原则

You can use Interception to add error handling. You don’t want to burden a Dependency with error handling. Because a Dependency should be viewed as a reusable component that can be consumed in many different scenarios, it wouldn’t be possible to add an exception-handling strategy to the Dependency that would fit all scenarios. It would also be a violation of the Single Responsibility Principle if you did.

09-08.tif

图 9.8 产品管理应用程序通过向用户显示消息来处理通信异常。请注意,在这种情况下,错误消息源自断路器而不是底层通信故障。

Figure 9.8 The product-management application handles communication exceptions by showing a message to the user. Notice that in this case, the error message originates from the Circuit Breaker instead of the underlying communication failure.

通过使用拦截处理异常,您遵循开放/封闭原则. 它允许您针对任何给定情况实施最佳错误处理策略。让我们看一个例子。

By using Interception to deal with exceptions, you follow the Open/Closed Principle. It allows you to implement the best error-handling strategy for any given situation. Let’s look at an example.

在前面的示例中,我们包装WcfProductRepository了一个断路器以用于产品管理客户端应用程序,该应用程序最初在 7.2.2 节中介绍过。断路器仅通过确保客户端快速失败来处理错误,但它仍然会抛出异常。如果不加以处理,它们将导致应用程序崩溃,因此您应该实现一个知道如何处理其中一些错误的装饰器。

In the previous example, we wrapped WcfProductRepository in a Circuit Breaker for use with the product-management client application, which was originally introduced in section 7.2.2. A Circuit Breaker only deals with errors by making certain that the client fails fast, but it still throws exceptions. If left unhandled, they’ll cause the application to crash, so you should implement a Decorator that knows how to handle some of those errors.

与崩溃的应用程序不同,您可能更喜欢一个消息框,告诉用户操作没有成功,他们应该稍后再试。在这个例子中,当抛出异常时,它应该弹出如图 9.8所示的消息。

Instead of a crashing application, you might prefer a message box that tells the user that the operation didn’t succeed and that they should try again later. In this example, when an exception is thrown, it should pop up a message as shown in figure 9.8.

实现这种行为很容易。与您在 9.2.1 节中所做的相同,您添加一个新ErrorHandlingProductRepositoryDecorator装饰IProductRepository界面. 清单 9.6显示了该接口方法之一的示例,但它们都很相似。

Implementing this behavior is easy. The same way you did in section 9.2.1, you add a new ErrorHandlingProductRepositoryDecorator class that decorates the IProductRepository interface. Listing 9.6 shows a sample of one of the methods of that interface, but they’re all similar.

清单 9.6 处理异常ErrorHandlingProductRepositoryDecorator

Listing 9.6 Handling exceptions with ErrorHandlingProductRepositoryDecorator

public void Insert(Product product)
{
    try
    {
        this.decoratee.Insert(product);    ①  
    }
    catch (CommunicationException ex)
    {
        this.AlertUser(ex.Message);    ②  
    }
    catch (InvalidOperationException ex)
    {
        this.AlertUser(ex.Message);    ②  
    }
}

Insert方法代表ErrorHandlingProductRepositoryDecorator类的整个实现. decoratee如果抛出异常,您将尝试调用并用错误消息警告用户。请注意,您只处理一组特定的已知异常,因为抑制所有异常可能很危险。提醒用户涉及格式化字符串并使用该方法将其显示给用户MessageBox.Show。这是在AlertUser方法内部完成的.

The Insert method is representative of the entire implementation of the ErrorHandlingProductRepositoryDecorator class. You attempt to invoke the decoratee and alert the user with the error message if an exception is thrown. Notice that you only handle a particular set of known exceptions, because it can be dangerous to suppress all exceptions. Alerting the user involves formatting a string and showing it to the user using the MessageBox.Show method. This is done inside the AlertUser method.

再一次,您WcfProductRepository通过实现装饰器模式向原始实现 () 添加了功能。您通过不断添加新类型而不是修改现有代码来遵循单一职责原则开放/封闭原则。到现在为止,您应该会看到一种模式,它暗示了一种比装饰器更通用的安排。让我们简要地看一下最后一个例子,实现安全性。

Once again, you added functionality to the original implementation (WcfProductRepository) by implementing the Decorator pattern. You’re following both the Single Responsibility Principle and the Open/Closed Principle by continually adding new types instead of modifying existing code. By now, you should be seeing a pattern that suggests a more general arrangement than a Decorator. Let’s briefly glance at a final example, implementing security.

9.2.3 使用装饰器防止未经授权访问敏感功能

9.2.3 Preventing unauthorized access to sensitive functionality using a Decorator

安全是另一个常见的交叉问题。我们希望尽可能保护我们的应用程序,以防止未经授权访问敏感数据和功能。

Security is another common Cross-Cutting Concern. We want to secure our applications as much as possible to prevent unauthorized access to sensitive data and functionality.

类似于我们使用 Circuit Breaker 的方式,我们想要拦截方法调用并检查是否允许调用。如果不是,则不应允许进行调用,而应抛出异常。原理是一样的;区别在于我们用来确定调用有效性的标准。

Similar to how we used Circuit Breaker, we’d like to Intercept a method call and check whether the call should be allowed. If not, instead of allowing the call to be made, an exception should be thrown. The principle is the same; the difference lies in the criterion we use to determine the validity of the call.

实现授权逻辑的一种常见方法是通过检查用户角色与手头操作的硬编码值来采用基于角色的安全性。如果我们坚持我们的IProductRepository,我们可能会从 a 开始。因为,正如您在前面几节中看到的,所有方法看起来都很相似,所以下面的清单只显示了两个方法实现。SecureProductRepositoryDecorator

A common approach to implementing authorization logic is to employ role-based security by checking the user’s role(s) against a hard-coded value for the operation at hand. If we stick with our IProductRepository, we might start out with a SecureProductRepositoryDecorator. Because, as you’ve seen in the previous sections, all methods look similar, the following listing only shows two method implementations.

清单 9.7 使用装饰器显式检查授权

Listing 9.7 Explicitly checking authorization with a Decorator

public class SecureProductRepositoryDecorator
    : IProductRepository
{
    private readonly IUserContext userContext;
    private readonly IProductRepository decoratee;

    public SecureProductRepositoryDecorator(
        IUserContext userContext,    ①  
        IProductRepository decoratee)
    {
        this.userContext = userContext;
        this.decoratee = decoratee;
    }

    public void Delete(Guid id)
    {
        this.CheckAuthorization();    ②  
        this.decoratee.Delete(id);
    }

    public IEnumerable<Product> GetAll()    ③  
    {
        return this.decoratee.GetAll();
    }
    ...
    private void CheckAuthorization()    ④  
    {    ④  
        if (!this.userContext.IsInRole(    ④  
            Role.Administrator))    ④  
        {    ④  
            throw new SecurityException(    ④  
                "Access denied.");    ④  
        }    ④  
    }    ④  
}

在我们目前的设计中,对于给定的Cross-Cutting Concern,基于 Decorator 的实现往往是重复的。实现断路器涉及将相同的代码模板应用于IProductRepository接口的所有方法。如果您想将断路器添加到另一个抽象,您将不得不将相同的代码应用于更多方法。

In our current design, for a given Cross-Cutting Concern, the implementation based on a Decorator tends to be repetitive. Implementing a Circuit Breaker involves applying the same code template to all methods of the IProductRepository interface. Had you wanted to add a Circuit Breaker to another Abstraction, you would’ve had to apply the same code to more methods.

使用安全装饰器时,情况变得更糟,因为我们需要扩展一些方法,而其他方法仅仅是传递操作。但总体问题是相同的。

With the security Decorator, it got even worse because we required some of the methods to be extended, whereas others are mere pass-through operations. But the overall problem is identical.

如果您需要将此Cross-Cutting Concern应用于不同的抽象,这也会导致代码重复,随着系统变大,这可能会导致主要的可维护性问题。正如您可能想象的那样,有一些方法可以防止代码重复,这将我们带到面向方面编程的重要主题,我们将在下一章中讨论。

If you need to apply this Cross-Cutting Concern to a different Abstraction, this too will cause code duplication, which can cause major maintainability issues as the system gets bigger. As you might imagine, there are ways to prevent code duplication, bringing us to the important topic of Aspect-Oriented Programming, which we’ll discuss in the next chapter.

概括

Summary

  • 拦截是拦截两个协作组件之间的调用的能力,这样您就可以丰富或更改依赖项的行为,而无需更改两个协作者本身。
  • Interception is the ability to intercept calls between two collaborating components in such a way that you can enrich or change the behavior of the Dependency without the need to change the two collaborators themselves.
  • 松散耦合是Interception的推动者。当您针对接口编程时,您可以通过将核心实现包装在该接口的其他实现中来转换或增强核心实现。
  • Loose coupling is the enabler of Interception. When you program to an interface, you can transform or enhance a core implementation by wrapping it in other implementations of that interface.
  • Interception的核心是 Decorator 设计模式的应用。
  • At its core, Interception is an application of the Decorator design pattern.
  • 装饰器设计模式通过动态地将附加职责附加到对象,为子类化提供了一种灵活的替代方法。它的工作原理是将Abstraction的一个实现包装在同一Abstraction的另一个实现中。这允许装饰器像俄罗斯嵌套娃娃一样嵌套。
  • The Decorator design pattern provides a flexible alternative to subclassing by attaching additional responsibilities to an object dynamically. It works by wrapping one implementation of an Abstraction in another implementation of the same Abstraction. This allows Decorators to be nested like Russian nesting dolls.
  • 横切关注点是代码的非功能方面,通常跨越代码库的广泛区域。横切关注点的常见示例是审计、日志记录、验证、安全和缓存。
  • Cross-Cutting Concerns are non-functional aspects of code that typically cut across a wide area of the code base. Common examples of Cross-Cutting Concerns are auditing, logging, validation, security, and caching.
  • 断路器是一种稳定性设计模式,它通过在发生故障时切断连接来增加系统的鲁棒性,以防止故障传播。
  • Circuit Breaker is a stability design pattern that adds robustness to a system by cutting connections when a fault occurs in order to prevent the fault from propagating.

10

设计的面向方面编程

10

Aspect-Oriented Programming by design

在这一章当中

In this chapter

  • 重述SOLID原则
  • Recapping the SOLID principles
  • 使用面向方面的编程来防止代码重复
  • Using Aspect-Oriented Programming to prevent code duplication
  • 使用SOLID实现面向切面编程
  • Using SOLID to achieve Aspect-Oriented Programming

在家做饭和在专业厨房工作有很大区别。在家里,您可以随心所欲地准备菜肴,但在商用厨房中,效率是关键。Mise en place是其中的一个重要方面。这不仅仅是提前准备配料;它是关于设置所有必需的设备,包括您的锅、平底锅、砧板、品尝勺,以及任何您工作空间必不可少的东西。

There’s a big difference between cooking at home and working in a professional kitchen. At home, you can take all the time you want to prepare your dish, but in a commercial kitchen, efficiency is key. Mise en place is an important aspect of this. This is more than in-advance preparation of ingredients; it’s about having all the required equipment set up, including your pots, pans, chopping boards, tasting spoons, and anything that’s an essential part of your workspace.

厨房的人体工程学和布局也是厨房效率的主要因素。布置不当的厨房可能会导致夹点、严重的中断和员工的上下文切换。配备相关专用设备的专用站等功能有助于最大限度地减少员工的流动,避免(不必要的)多任务处理,并鼓励专注于手头的任务。如果做得好,有助于提高整个厨房的效率。

The ergonomics and layout of the kitchen is also a major factor in the efficiency of a kitchen. A badly laid out kitchen can cause pinch points, high levels of disruption, and context switching for staff. Features like dedicated stations with associated specialized equipment help to minimize the movement of staff, avoid (unnecessary) multitasking, and encourage concentration on the task at hand. When this is done well, it helps to improve the efficiency of the kitchen as a whole.

在软件开发中,代码库就是我们的厨房。团队在同一个厨房里一起工作多年,正确的架构对于高效和一致至关重要,将代码重复保持在最低限度。您的“客人”取决于您成功的厨房策略。

In software development, the code base is our kitchen. Teams work together for years in the same kitchen, and the right architecture is essential to be efficient and consistent, keeping code repetition to a minimum. Your “guests” depend on your successful kitchen strategy.

可以用来改进软件人体工程学的关键架构策略之一是面向方面的编程(AOP). 这可以以设备(工具)或实体布局(软件设计)的形式出现。AOP 与Interception密切相关。要充分发挥Interception的潜力,您必须研究 AOP 的概念和SOLID等软件设计原则。

One of the key architectural strategies you can use to improve your software ergonomics is Aspect-Oriented Programming (AOP). This can come in the form of equipment (tools) or a solid layout (software design). AOP is strongly related to Interception. To fully appreciate the potential of Interception, you must study the concept of AOP and software design principles like SOLID.

本章首先介绍 AOP。由于应用 AOP 的最有效方法之一是通过众所周知的设计模式和面向对象的原则,因此本章继续回顾本书前几章讨论的五个SOLID原则。

This chapter starts with an introduction to AOP. Because one of the most effective ways to apply AOP is through well-known design patterns and object-oriented principles, this chapter continues with a recap of the five SOLID principles, which were discussed in previous chapters throughout the book.

一个常见的误解是 AOP 需要工具。在本章中,我们将证明情况并非如此:我们将展示如何使用SOLID软件设计作为 AOP 的驱动程序和高效、一致且可维护的代码库的推动者。在下一章中,我们将讨论需要特殊工具的两种著名的 AOP 形式。然而,与本章讨论的纯设计驱动的 AOP 形式相比,这两种形式都表现出相当大的缺点。

A common misconception is that AOP requires tooling. In this chapter, we’ll demonstrate that this isn’t the case: We’ll show how you can use SOLID software design as a driver of AOP and an enabler of an efficient, consistent, and maintainable code base. In the next chapter, we’ll discuss two well-known forms of AOP that require special tooling. Both forms, however, exhibit considerable disadvantages over the purely design-driven form of AOP discussed in this chapter.

如果您已经熟悉SOLID和 AOP 的基础知识,您可以直接跳到第 10.3 节,其中包含本章的内容。否则,您可以继续我们对面向方面编程的介绍。

If you’re already familiar with SOLID and the basics of AOP, you can jump directly into section 10.3, which contains the meat of this chapter. Otherwise, you can continue with our introduction to Aspect-Oriented Programming.

10.1 介绍AOP

10.1 Introducing AOP

AOP 于 1997 年在施乐帕洛阿尔托研究中心 (PARC) 发明,施乐工程师在那里设计了 AspectJ,它是 Java 语言的 AOP 扩展。AOP 是一种范例,它侧重于有效且可维护地应用横切关注点的概念。这是一个相当抽象的概念,有自己的一套行话,其中大部分与本次讨论无关。

AOP was invented at the Xerox Palo Alto Research Center (PARC) in 1997, where Xerox engineers designed AspectJ, an AOP extension to the Java language. AOP is a paradigm that focuses around the notion of applying Cross-Cutting Concerns effectively and maintainably. It’s a fairly abstract concept that comes with its own set of jargon, most of which isn’t pertinent to this discussion.

9.1.2 和 9.2.1 节中的审计和断路器示例仅显示了几个具有代表性的方法,因为所有方法都是以相同的方式实现的。我们不想在我们的讨论中添加几页几乎相同的代码,因为这会分散我们的注意力。

The auditing and Circuit Breaker examples in sections 9.1.2 and 9.2.1 showed only a few representative methods, because all methods were implemented in the same way. We didn’t want to add several pages of nearly identical code to our discussion because it would’ve detracted from the point we were making.

下面的清单再次显示了CircuitBreakerProductRepositoryDecoratorDelete方法。

The following listing shows the CircuitBreakerProductRepositoryDecorator’s Delete method again.

清单 10.1 Delete方法CircuitBreakerProductRepositoryDecorator

Listing 10.1 Delete method of CircuitBreakerProductRepositoryDecorator

public void Delete(Product product)
{
    this.breaker.Guard();

    try
    {
        this.decoratee.Delete(product);
        this.breaker.Succeed();
    }
    catch (Exception ex)
    {
        this.breaker.Trip(ex);
        throw;
    }
}

清单 10.2显示了 的方法有多么相似CircuitBreakerProductRepositoryDecorator。此清单仅显示Insert方法,但我们相信您可以推断其余实现的外观。

Listing 10.2 shows how similar the methods of CircuitBreakerProductRepositoryDecorator are. This listing only shows the Insert method, but we’re confident that you can extrapolate how the rest of the implementation would look.

气味.tif

清单 10.2 通过复制断路器逻辑违反 DRY 原则

Listing 10.2 Violating the DRY principle by duplicating Circuit Breaker logic

public void Insert(Product product)
{
    this.breaker.Guard();

    try
    {
        this.decoratee.Insert(product);    ①  
        this.breaker.Succeed();
    }
    catch (Exception ex)
    {
        this.breaker.Trip(ex);
        throw;
    }
}

这个清单的目的是说明在我们当前的设计中用作方面的装饰器的重复性。Delete和方法之间的唯一区别Insert是它们各自在装饰的 Repository 上调用自己相应的方法。

The purpose of this listing is to illustrate the repetitive nature of Decorators used as aspects in our current design. The only difference between the Delete and Insert methods is that they each invoke their own corresponding method on the decorated Repository.

ICircuitBreaker即使我们已经通过接口成功地将断路器实现委托给一个单独的类,此管道代码违反了 DRY 原则。它往往是合理不变的,但它仍然是一种负担。每次你想向你装饰的类型添加一个新成员,或者当你想将一个断路器应用于一个新的抽象时,你必须应用相同的管道代码。如果您想维护这样的应用程序,这种重复性可能会成为一个问题。

Even though we’ve successfully delegated the Circuit Breaker implementation to a separate class via the ICircuitBreaker interface, this plumbing code violates the DRY principle. It tends to be reasonably unchanging, but it’s still a liability. Every time you want to add a new member to a type you decorate, or when you want to apply a Circuit Breaker to a new Abstraction, you must apply the same plumbing code. This repetitiveness can become a problem if you want to maintain such an application.

继续我们第 9 章的审计示例,我们已经确定您不想将审计代码放在SqlProductRepository实现中,因为那样会违反单一职责原则(SRP)。但是你也不希望系统中的每个存储库抽象都有几十个审计装饰器。这还会导致严重的代码重复,并可能导致彻底的更改,这违反了开放/封闭原则(OCP)。相反,您想声明性地声明您要将审计方面应用于系统中所有存储库抽象的一组特定方法,并一次实现此审计方面。

Sticking with our auditing example from chapter 9, we’ve already established that you don’t want to put the auditing code inside the SqlProductRepository implementation, because that would violate the Single Responsibility Principle (SRP). But neither do you want to have dozens of auditing Decorators for each Repository Abstraction in the system. This would also cause severe code duplication and, likely, sweeping changes, which is an Open/Closed Principle (OCP) violation. Instead, you want to declaratively state that you want to apply the auditing aspect to a certain set of methods of all Repository Abstractions in the system and implement this auditing aspect once.

您将找到支持 AOP 的工具、框架和体系结构样式。在本章中,我们将讨论最理想的 AOP 形式。下一章将讨论动态拦截和编译时编织作为 AOP 的基于工具的形式。这就是AOP的三大方法。1  表 10.1列出了我们将讨论的方法,以及每种方法的一些主要优点和缺点。

You’ll find tools, frameworks, and architectural styles that enable AOP. In this chapter, we’ll discuss the most ideal form of AOP. The next chapter will discuss dynamic Interception and compile-time weaving as tool-based forms of AOP. These are the three major methods of AOP.1  Table 10.1 lists the methods we’ll discuss, with a few of the major advantages and disadvantages of each.

表 10.1 常见的 AOP 方法
方法描述优点缺点
坚硬的 使用装饰器围绕基于类的行为为类组定义的可重用抽象应用方面。
  • 不需要任何工具。
  • Doesn’t require any tooling.
  • 方面很容易实现。
  • Aspects are easy to implement.
  • 专注于设计。
  • Focuses on design.
  • 使系统更易于维护。
  • Makes the system more maintainable.
  • 在遗留系统中应用并不总是那么容易。
  • Not always easy to apply in legacy systems.
动态拦截 导致运行时生成基于应用程序抽象的装饰器。这些装饰器注入了特定于工具的方面,称为拦截器
  • 假设应用程序已经编程到接口,则可以通过相对较少的更改轻松添加到现有或遗留应用程序。
  • Easy to add to existing or legacy applications with relatively little changes, assuming the application already programs to interfaces.
  • 使编译后的应用程序与使用的动态拦截库分离
  • Keeps the compiled application decoupled from the used dynamic Interceptionlibrary
  • 好的工具是免费提供的。
  • Good tooling is freely available.
  • 导致方面与 AOP 工具强耦合。
  • Causes aspects to be strongly coupled to the AOP tool.
  • 失去编译时支持。
  • Loses compile-time support.
  • 导致约定脆弱且容易出错。
  • Causes the convention to be fragile and error prone.
编译时编织 方面是在编译后过程中添加到应用程序中的。最常见的形式是 IL 编织,其中外部工具读取已编译的程序集,通过应用方面对其进行修改,并用修改后的程序集替换原始程序集。
  • 只需相对较少的更改即可轻松添加到现有或遗留应用程序中,即使应用程序不针对接口进行编程也是如此。
  • Easy to add to existing or legacy applications with relatively few changes, even if the application doesn’t program to interfaces.
  • 将易失性依赖项注入方面会导致时间耦合或相互依赖测试。
  • Injecting Volatile Dependencies into aspects causes Temporal Coupling or Interdependent Tests.
  • 方面是在编译时编织的,如果没有应用方面就不可能调用代码。这使测试复杂化并降低了灵活性。
  • Aspects are woven in at compile time, making it impossible to call code without the aspect applied. This complicates testing and reduces flexibility.
  • 编译时编织是 DI 的对立面。
  • Compile-time weaving is the antithesis of DI.

如前所述,我们将在下一章回到动态拦截和编译时编织。但在我们深入研究使用SOLID作为 AOP 的驱动程序之前,让我们先简要回顾一下SOLID原则。

As stated previously, we’ll get back to dynamic Interception and compile-time weaving in the next chapter. But before we dive into using SOLID as a driver for AOP, let’s start with a short recap of the SOLID principles.

10.2 SOLID原则

10.2 The SOLID principles

您可能已经注意到,在第 9 章和上一节中,单一职责原则开放/封闭原则Liskov 替换原则等术语的使用比平常更密集。连同接口隔离原则(ISP) 和依赖倒置原则(DIP), 它们构成了SOLID的缩写。在本书的整个过程中,我们已经独立地讨论了所有这五个原则,但是本节提供了一个简短的总结来刷新您的想法,因为理解这些原则对于本章的其余部分很重要。

You may have noticed a denser-than-usual usage of terms such as Single Responsibility Principle, Open/Closed Principle, and Liskov Substitution Principle in chapter 9 and in the previous section. Together with the Interface Segregation Principle (ISP) and Dependency Inversion Principle (DIP), they make up the SOLID acronym. We’ve discussed all five of them independently throughout the course of this book, but this section provides a short summary to refresh your mind, because understanding those principles is important for the remainder of this chapter.

所有这些模式和原则都被认为是编写干净代码的宝贵指南。本节的一般目的是将这一既定指南与 DI 联系起来,强调 DI 只是达到目的的一种手段。因此,我们使用 DI 作为可维护代码的推动者。

All these patterns and principles are recognized as valuable guidance for writing clean code. The general purpose of this section is to relate this established guidance to DI, emphasizing that DI is only a means to an end. We, therefore, use DI as an enabler of maintainable code.

SOLID所包含的原则都不是绝对的。它们是可以帮助您编写干净代码的指南。对我们来说,它们代表了帮助我们决定应用程序应朝哪个方向发展的目标。当我们成功时,我们总是很高兴;但有时我们不这样做。

None of the principles encapsulated by SOLID represent absolutes. They’re guidelines that can help you write clean code. To us, they represent goals that help us decide which direction we should take our applications. We’re always happy when we succeed; but sometimes we don’t.

以下各节介绍了SOLID原则,并总结了我们在本书的整个过程中已经解释过的内容。每个部分都是一个简短的概述——我们在这些部分中省略了示例。我们将在 10.3 节中回到这一点,我们将通过一个真实的例子来说明为什么从可维护性的角度来看违反SOLID原则会成为问题。现在,我们将重述五个SOLID原则。

The following sections go through the SOLID principles and summarize what we’ve already explained about them throughout the course of this book. Each section is a brief overview — we omit examples in those sections. We’ll return to this in section 10.3, where we walk through a realistic example that shows why a violation of the SOLID principles can become problematic from a maintainability perspective. For now, we’ll recap the five SOLID principles.

10.2.1 单一职责原则(SRP)

10.2.1 Single Responsibility Principle (SRP)

在 2.1.3 节中,我们描述了 SRP 如何声明每个类都应该有一个单一的更改理由。违反这个原则会导致类变得更复杂,更难测试和维护。

In section 2.1.3, we described how the SRP states that every class should have a single reason to change. Violating this principle causes classes to become more complex and harder to test and maintain.

然而,通常情况下,查看一个类是否有多个更改原因可能具有挑战性。在这方面可以提供帮助的是从凝聚力的角度审视 SRP。凝聚被定义为类或模块的元素的功能相关性。相关性越低,凝聚力越低;凝聚力越低,类违反 SRP 的可能性就越大。在 10.3 节中,我们将通过一个具体示例来讨论内聚。

More often than not, however, it can be challenging to see whether a class has multiple reasons to change. What can help in this respect is looking at the SRP from the perspective of cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the amount of relatedness, the lower the cohesion; and the lower the cohesion, the greater the possibility a class violates the SRP. In section 10.3, we’ll discuss cohesion with a concrete example.

它可能很难坚持,但如果你练习 DI,构造函数注入的众多好处之一就是当你违反 SRP 时它会变得更加明显。在 9.1.2 节的审计示例中,您可以通过将职责分为不同的类型来遵守 SRP:SqlUserRepository仅处理存储和检索产品数据,而专注于在数据库中持久保存审计线索。该类的唯一职责是协调 和 的动作。AuditingUserRepositoryDecoratorAuditingUserRepositoryDecoratorIUserRepositoryIAuditTrailAppender

It can be difficult to stick to, but if you practice DI, one of the many benefits of Constructor Injection is that it becomes more obvious when you violate the SRP. In the auditing example in section 9.1.2, you were able to adhere to the SRP by separating responsibilities into separate types: SqlUserRepository deals only with storing and retrieving product data, whereas AuditingUserRepositoryDecorator concentrates on persisting the audit trail in the database. The AuditingUserRepositoryDecorator class’s single responsibility is to coordinate the actions of IUserRepository and IAuditTrailAppender.

10.2.2 开闭原则(OCP)

10.2.2 Open/Closed Principle (OCP)

正如我们在 4.4.2 节中讨论的那样,OCP 规定了一种应用程序设计,可以防止您必须对整个代码库进行彻底的更改;或者,在 OCP 的词汇表中,一个类应该对扩展开放,但对修改关闭。开发人员应该能够扩展系统的功能,而无需修改任何现有类的源代码。

As we discussed in section 4.4.2, the OCP prescribes an application design that prevents you from having to make sweeping changes throughout the code base; or, in the vocabulary of the OCP, a class should be open for extension, but closed for modification. A developer should be able to extend the functionality of a system without needing to modify the source code of any existing classes.

因为他们都试图阻止彻底的改变,所以 OCP 原则和 Don’t 原则之间有很强的关系重复自己(DRY)原则。然而,OCP 侧重于代码,而 DRY 侧重于知识。

Because they both try to prevent sweeping changes, there’s a strong relationship between the OCP principle and the Don’t Repeat Yourself (DRY) principle. OCP, however, focuses on code, whereas DRY focuses on knowledge.

您可以通过多种方式使类具有可扩展性,包括虚方法、Strategies 的注入以及装饰器的应用。3  但无论细节如何,DI 通过使您能够组合对象而使这成为可能。

You can make a class extensible in many ways, including virtual methods, injection of Strategies, and the application of Decorators.3  But no matter the details, DI makes this possible by enabling you to compose objects.

10.2.3 里氏替换原则(LSP)

10.2.3 Liskov Substitution Principle (LSP)

在 8.1.1 节中,我们描述了依赖项的所有消费者在调用它们的依赖项时都应该遵守 LSP 因为每个依赖项都应该按照其抽象定义的方式运行。这允许您用相同抽象的另一个实现替换最初预期的实现,而不用担心破坏消费者。因为装饰器实现了与它包装的类相同的抽象,所以您可以用装饰器替换原来的抽象,但前提是该装饰器遵守其抽象给出的契约。

In section 8.1.1, we described that all consumers of Dependencies should observe the LSP when they invoke their Dependencies ,because every Dependency should behave as defined by its Abstraction. This allows you to replace the originally intended implementation with another implementation of the same Abstraction, without worrying about breaking a consumer. Because a Decorator implements the same Abstraction as the class it wraps, you can replace the original with a Decorator, but only if that Decorator adheres to the contract given by its Abstraction.

这正是我们在清单 9.3SqlUserRepository中用. 您可以在不更改 consuming 的代码的情况下执行此操作,因为任何实现都应遵守 LSP。需要一个实例,只要它专门与该接口对话,任何实现都可以。AuditingUserRepositoryDecoratorProductServiceProductServiceIUserRepository

This was exactly what we did in listing 9.3 when we substituted the original SqlUserRepository with AuditingUserRepositoryDecorator. You could do this without changing the code of the consuming ProductService, because any implementation should adhere to the LSP. ProductService requires an instance of IUserRepository and, as long as it talks exclusively to that interface, any implementation will do.

LSP 是 DI 的基础。当消费者不观察它时,注入Dependencies几乎没有优势,因为您不能随意替换它们,并且您将失去 DI 的许多(如果不是全部)好处。

The LSP is a foundation of DI. When consumers don’t observe it, there’s little advantage in injecting Dependencies, because you can’t replace them at will, and you’ll lose many (if not all) benefits of DI.

10.2.4 接口隔离原则(ISP)

10.2.4 Interface Segregation Principle (ISP)

在 6.2.1 节中,您了解到 ISP 提倡使用细粒度抽象,而不是宽抽象。任何时候消费者依赖于其某些成员未被使用的抽象,都会违反 ISP。

In section 6.2.1, you learned that the ISP promotes the use of fine-grained Abstractions, rather than wide Abstractions. Any time a consumer depends on an Abstraction where some of its members are unused, the ISP is violated.

乍一看,ISP 似乎与 DI 关系很远,但这可能是因为我们在本书的大部分内容中都忽略了这一原则。这将在 10.3 节中改变,您将在该节中了解到 ISP 在有效应用面向方面编程时至关重要。

The ISP can, at first, seem to be distantly related to DI, but that’s probably because we ignored this principle for most of this book. That’ll change in section 10.3, where you’ll learn that the ISP is crucial when it comes to effectively applying Aspect-Oriented Programming.

10.2.5 依赖倒置原则(DIP)

10.2.5 Dependency Inversion Principle (DIP)

当我们在 3.1.2 节中讨论 DIP 时,您了解到我们试图用 DI 完成的大部分工作都与 DIP 相关。该原则指出您应该针对Abstractions进行编程,并且消费层应该控制消费Abstraction的形状。消费者应该能够以最有利于自己的方式定义抽象。如果您发现自己向接口添加成员以满足其他特定实现(包括未来可能的实现)的需要,那么您几乎肯定违反了 DIP。

When we discussed the DIP in section 3.1.2, you learned that much of what we’re trying to accomplish with DI is related to the DIP. The principle states that you should program against Abstractions, and that the consuming layer should be in control of the shape of a consumed Abstraction. The consumer should be able to define the Abstraction in a way that benefits itself the most. If you find yourself adding members to an interface to satisfy the needs of other, specific implementations — including potential future implementations — then you’re almost certainly violating the DIP.

10.2.6 SOLID原则和拦截

10.2.6 SOLID principles and Interception

设计模式(如 Decorator)和指南(如SOLID原则)已经存在多年,通常被认为是有益的。在这些部分中,我们提供了它们与 DI 的关系的说明。

Design patterns (such as Decorator) and guidelines (such as SOLID principles) have been around for many years and are generally regarded as beneficial. In these sections, we provide an indication of how they relate to DI.

SOLID原则贯穿本书的各个章节。但是当我们开始谈论拦截器以及它与装饰器的关系时,遵守SOLID原则的好处就凸显出来了。有些比其他的更微妙,但是通过使用装饰器添加行为(例如审计)是 OCP 和 SRP 的明显应用,后者允许我们创建具有特定定义范围的实现。

The SOLID principles have been relevant throughout the book’s chapters. But it’s when we start talking about Interception and how it relates to Decorators that the benefits of adhering to the SOLID principles stands out. Some are subtler than others, but adding behavior (such as auditing) by using a Decorator is a clear application of both the OCP and the SRP, the latter allowing us to create implementations with specifically defined scopes.

在前面的部分中,我们简要介绍了常见的模式和原则,以了解 DI 与其他既定指南的关系。有了这些知识,现在让我们将注意力转回本章的目标,即在面对不一致或不断变化的需求时编写干净且可维护的代码,以及解决横切关注点的需要。

In the previous sections, we took a short detour through common patterns and principles to understand the relationship DI has with other established guidelines. Armed with this knowledge, let’s now turn our attention back to the goal of the chapter, which is to write clean and maintainable code in the face of inconsistent or changing requirements, as well as the need to address Cross-Cutting Concerns.

10.3 SOLID作为 AOP 的驱动程序

10.3 SOLID as a driver for AOP

在 10.1 节中,您了解到 AOP 的主要目标是保持您的横切关注点DRY。正如我们在 10.2 节中讨论的那样,OCP 和 DRY 原则之间存在着密切的关系。他们都为同一个目标而努力,即尽量减少重复并防止彻底改变。

In section 10.1, you learned that the primary aim of AOP is to keep your Cross-Cutting Concerns DRY. As we discussed in section 10.2, there’s a strong relationship between the OCP and the DRY principle. They both strive for the same objective, which is to minimize repetition and prevent sweeping changes.

从这个角度来看,您在第 9 章(清单 9.2、9.4 和 9.7)中看到的 、 和 代码重复强烈表明我们违反了 OCP。AOP 试图通过将可扩展的行为(方面)分离成单独的组件来解决这个问题,这些组件可以很容易地应用于各种实现。AuditingUserRepositoryDecoratorCircuitBreakerProductRepositoryDecoratorSecureProductRepositoryDecorator

From that perspective, the code repetition that you witnessed with AuditingUserRepositoryDecorator, CircuitBreakerProductRepositoryDecorator, and SecureProductRepositoryDecorator in chapter 9 (listings 9.2, 9.4, and 9.7) are a strong indication that we were violating the OCP. AOP seeks to address this by separating out extensible behavior (aspects) into separate components that can easily be applied to a variety of implementations.

然而,一个常见的误解是 AOP 需要工具。AOP 工具供应商都急于让这个谬论继续存在。我们首选的方法是通过设计实践 AOP,这意味着您首先应用模式和原则,然后再恢复到动态拦截库等专门的 AOP 工具。

A common misconception, however, is that AOP requires tooling. AOP tool vendors are all to eager to keep this fallacy alive. Our preferred approach is to practice AOP by design, which means you apply patterns and principles first, before reverting to specialized AOP tooling like dynamic Interception libraries.

在本节中,我们将这样做。我们将从设计的角度审视 AOP,通过仔细研究我们在第 3 章中介绍的IProductService 抽象。我们将分析我们违反了哪些SOLID原则,以及为什么此类违反会产生问题。之后,我们将逐步解决这些违规问题,目的是使应用程序更易于维护,从而避免将来需要进行彻底的更改。为一些精神上的不适——甚至认知失调——做好准备,因为我们违背了你对如何设计软件的信念。系好安全带,准备乘车。

In this section, we’ll do just that. We’ll look at AOP from a design perspective by taking a close look at the IProductService Abstraction we introduced in chapter 3. We’ll analyze which SOLID principles we’re violating and why such violations are problematic. After that, we’ll address these violations step by step with the goal of making the application more maintainable, preventing the need to make sweeping changes in the future. Be prepared for some mental discomfort — and even cognitive dissonance — as we defy your beliefs on how to design software. Buckle up, and get ready for the ride.

10.3.1 示例:使用 IProductService 实现与产品相关的功能

10.3.1 Example: Implementing product-related features using IProductService

让我们通过查看您在第 3 章中作为示例电子商务应用程序域层的一部分构建的IProductService 抽象来深入研究。以下清单显示了最初在清单 3.5 中定义的接口。

Let’s dive right in by looking at the IProductService Abstraction that you built in chapter 3 as part of the sample e-commerce application’s domain layer. The following listing shows this interface as originally defined in listing 3.5.

清单 10.3IProductService第 3 章 的界面

Listing 10.3 The IProductService interface of chapter 3

public interface IProductService
{
    IEnumerable<DiscountedProduct> GetFeaturedProducts();
}

当从一般的SOLID原则(尤其是 OCP)的角度审视应用程序的设计时,重要的是要考虑应用程序如何随时间发生变化,并据此预测未来的变化。考虑到这一点,您可以确定应用程序是否关闭以修改将来最有可能发生的更改。

When looking at an application’s design from the perspective of SOLID principles in general, and the OCP in particular, it’s important to take into consideration how the application has changed over time, and from there predict future changes. With this in mind, you can determine whether the application is closed for modification to the changes that are most likely to happen in the future.

重要的是要注意,即使使用SOLID设计,也可能会有彻底改变的时候。100% 关闭修改既不可能也不可取。此外,符合 OCP 的成本很高。寻找和设计合适的抽象需要付出相当大的努力,尽管过多的抽象会对应用程序的复杂性产生负面影响。您的工作是平衡风险和成本,并得出全局最优解。

It’s important to note that even with a SOLID design, there can come a time where a change becomes sweeping. Being 100% closed for modification is neither possible nor desirable. Besides, conforming to the OCP is expensive. It takes considerable effort to find and design the appropriate Abstractions, although too many Abstractions can have a negative impact on the complexity of the application. Your job is to balance the risks and the costs and come up with a global optimum.

因为您应该查看应用程序的演变方式,所以IProductService在单个时间点进行评估并没有多大帮助。幸运的是,Mary Rowan(我们第 2 章的开发人员)已经在她的电子商务应用程序上工作了一段时间,并且自从我们上次查看她的肩上以来已经实现了许多功能。下一个清单显示了 Mary 的进步情况。

Because you should be looking at how the application evolves, evaluating IProductService at a single point in time isn’t that helpful. Fortunately, Mary Rowan (our developer from chapter 2), has been working on her e-commerce application for some time now, and a number of features have been implemented since we last looked over her shoulder. The next listing shows how Mary has progressed.

坏.tif

清单 10.4 进化后的IProductService接口

Listing 10.4 The evolved IProductService interface

public interface IProductService
{
    IEnumerable<DiscountedProduct> GetFeaturedProducts();

    void DeleteProduct(Guid productId);    ①  
    Product GetProductById(Guid productId);    ①  
    void InsertProduct(Product product);    ①  
    void UpdateProduct(Product product);    ①  
    Paged<Product> SearchProducts(    ①  
        int pageIndex, int pageSize,    ①  
        Guid? manufacturerId, string searchText);    ①  
    void UpdateProductReviewTotals(    ①  
        Guid productId, ProductReview[] reviews);    ①  
    void AdjustInventory(    ①  
        Guid productId, bool decrease, int quantity);    ①  
    void UpdateHasTierPricesProperty(Product product);    ①  
    void UpdateHasDiscountsApplied(    ①  
        Guid productId, string discountDescription);    ①  
}

如您所见,该应用程序中添加了许多新功能。有些是典型的 CRUD 操作,例如UpdateProduct,而另一些则处理更复杂的用例,例如. 还有一些用于检索数据,例如和。UpdateHasTierPricesPropertySearchProductsGetProductById

As you can see, quite a few new features have been added to the application. Some are typical CRUD operations, such as UpdateProduct, whereas others address more-complex use cases, such as UpdateHasTierPricesProperty. Still others are for retrieving data, such as SearchProducts and GetProductById.

IProductService虽然 Mary在定义清单 10.3的第一个版本时是出于好意,但每次实现与产品相关的新功能时都需要更新此接口这一事实清楚地表明出了问题。

Although Mary started off with good intentions when she defined the first version of IProductService in listing 10.3, the fact that this interface needs to be updated every time a new product-related feature is implemented is a clear indication that something’s wrong.

如果以此来推断一下,是不是可以期待这个界面很快就会再次更新呢?这个问题的答案是明确的“是!” 事实上,Mary 在她的积压工作中已经有几个功能,涉及交叉销售、产品图片和产品评论,这些都会导致对IProductService. 4个 

If you extrapolate this to make a prediction, can you expect this interface to be updated again soon? The answer to that question is a clear “Yes!” As a matter of fact, Mary already has several features in her backlog, concerning cross-sellings, product pictures, and product reviews that would all cause changes to IProductService.4 

这告诉我们,在这个特定的应用程序中,有关产品的新功能会定期添加。因为这是一个电子商务应用程序,所以这并不是一个惊天动地的观察结果。但由于这既是代码库的核心部分,又经常更改,因此需要改进设计。让我们牢记SOLID原则来分析当前的设计。

What this teaches us is that, in this particular application, new features concerning products are added on a regular basis. Because this is an e-commerce application, this isn’t a world-shattering observation. But because this is both a central part of the code base and under frequent change, the need to improve the design arises. Let’s analyze the current design with SOLID principles in mind.

10.3.2 从SOLID角度分析IProductService

10.3.2 Analysis of IProductService from the perspective of SOLID

关于 10.2 节中讨论的五项SOLID原则,Mary 的设计违反了五项SOLID原则中的三项,即 ISP、SRP 和 OCP。我们将从第一个开始:IProductService违反 ISP。

Concerning the five SOLID principles discussed in section 10.2, Mary’s design violates three out of five SOLID principles, namely, the ISP, SRP, and OCP. We’ll start with the first one: IProductService violates the ISP.

IProductService违反ISP

IProductService violates the ISP

有一个明显的违规行为——IProductService违反了 ISP。如第 10.2.4 节所述,ISP 规定在宽抽象上使用细粒度抽象。从ISP的角度来看,范围比较广。考虑到清单 10.4,很容易相信不会有任何一个消费者会使用它的所有方法。大多数消费者通常最多使用一种或几种方法。但是,这种违规行为怎么会成为问题呢?IProductServiceIProductService

There’s one obvious violation — IProductService violates the ISP. As explained in section 10.2.4, the ISP prescribes the use of fine-grained Abstractions over wide Abstractions. From the perspective of the ISP, IProductService is rather wide. With listing 10.4 in mind, it’s easy to believe that there’ll be no single consumer of IProductService that’ll use all its methods. Most consumers would typically use one method or a few at most. But how is this violation a problem?

宽接口直接导致问题的代码库的一部分是在测试期间。HomeController例如,'s 的单元测试将定义一个IProductServiceTest Double 实现,但需要这样一个 Test Double 来实现其所有成员,即使它HomeController本身只使用一种方法。5  即使您可以创建可重用的测试替身,您通常仍希望断言 的不相关方法IProductService未被调用HomeController。以下清单显示了一个 MockIProductService实现,它断言未调用意外方法。

A part of the code base where wide interfaces directly cause trouble is during testing. HomeController’s unit tests, for instance, will define an IProductService Test Double implementation, but such a Test Double is required to implement all its members, even though HomeController itself only uses one method.5  Even if you could create a reusable Test Double, you typically still want to assert that unrelated methods of IProductService aren’t called by HomeController. The following listing shows a Mock IProductService implementation that asserts unexpected methods aren’t called.

坏.tif

清单 10.5 一个可重用的 MockIProductService基类

Listing 10.5 A reusable Mock IProductService base class

public abstract class MockProductService : IProductService
{
    public virtual void DeleteProduct(Guid productId)
    {
        Assert.True(false, "Should not be called.");    ①  
    }

    public virtual Product GetProductById(Guid id)
    {
        Assert.True(false, "Should not be called.");    ①  
        return null;
    }

    public virtual void InsertProduct(Product product)
    {
        Assert.True(false, "Should not be called.");    ①  
    }

    ...    ②  
}

Assert.True通过使用 的值调用,所有方法都被实现为失败false。该Assert.True方法是 xUnit 测试框架的一部分。6  通过 passing false,断言失败,当前运行的测试也失败。

All methods are implemented to fail by calling Assert.True using a value of false. The Assert.True method is part of the xUnit testing framework.6  By passing false, the assertion fails, and the currently running test also fails.

为了保护珍贵的树木,清单 10.5只展示了一些MockProductService的方法,但我们认为您已经了解了。如果接口特定于 的需要,您就不必实现这一大列失败方法HomeController;在这种情况下,HomeController预计会调用其所有Dependency的方法,并且您不必进行此检查。

To preserve precious trees, listing 10.5 only shows a few of MockProductService’s methods, but we think you get the picture. You wouldn’t have to implement this big list of failing methods if the interface was specific to HomeController’s needs; in that case, HomeController is expected to call all its Dependency’s methods, and you wouldn’t have to do this check.

IProductService违反了 SRP

IProductService violates the SRP

因为 ISP 是 SRP 的概念基础,所以 ISP 违规通常表示其实现中存在 SRP 违规,就像这里的情况一样。SRP 违规有时很难检测到,您可能会争辩说ProductService实施有一个责任,即处理与产品相关的用例。

Because the ISP is the conceptual underpinning of the SRP, an ISP violation typically indicates an SRP violation in its implementations, as is the case here. SRP violations can sometimes be hard to detect, and you might argue that a ProductService implementation has one responsibility, namely, handling product-related use cases.

然而,与产品相关的用例的概念极其模糊和广泛。相反,您希望类只有一个更改原因。ProductService肯定有多种原因需要改变。例如,以下任一原因导致ProductService变更:

The concept of product-related use cases, however, is extremely vague and broad. Rather, you want classes that have only one reason to change. ProductService definitely has multiple reasons to change. For instance, any of the following reasons causes ProductService to change:

  • 折扣应用方式的变化
  • Changes to how discounts are applied
  • 更改库存调整的处理方式
  • Changes to how inventory adjustments are processed
  • 添加产品搜索条件
  • Adding search criteria for products
  • 添加与产品相关的新功能
  • Adding a new product-related feature

不仅ProductService有很多改变的理由,而且它的方法很可能没有凝聚力。发现低内聚的一个简单方法是检查将类的某些功能移动到新类的难易程度。这越容易,两个部分的相关性越低,违反 SRP 的可能性就越大。

Not only does ProductService have many reasons to change, its methods are most likely not cohesive. A simple way to spot low cohesion is to check how easy it is to move some of the class’s functionality to a new class. The easier this is, the lower the relatedness of the two parts, and the more likely SRP is violated.

也许和共享相同的Dependencies,但仅此而已;他们没有凝聚力。因此,该类可能会很复杂,从而导致可维护性问题。因此,应该分成多个类别。但这提出了一个问题:如果有的话,应该将多少个类和哪些方法组合在一起?在我们开始之前,让我们首先检查一下周围的设计是如何违反 OCP 的。UpdateHasTierPricesPropertyUpdateHasDiscountsAppliedProductServiceIProductService

Perhaps UpdateHasTierPricesProperty and UpdateHasDiscountsApplied share the same Dependencies, but that’d be about it; they aren’t cohesive. As a result, the class will likely be complex, which can cause maintainability problems. ProductService should, therefore, be split into multiple classes. But that raises this question: how many classes and which methods should be grouped together, if any? Before we get into that, let’s first inspect how the design around IProductService violates the OCP.

IProductService违反了OCP

IProductService violates the OCP

要测试代码是否违反了 OCP,您首先必须确定您可以预期对应用程序的这一部分进行什么样的更改。之后,你可以问这个问题,“当预期的变化发生时,这个设计是否会引起彻底的变化?”

To test whether the code violates the OCP, you first have to determine what kind of changes to this part of the application you can expect. After that, you can ask the question, “Does this design cause sweeping changes when expected changes are made?”

您可以预期在电子商务应用程序的生命周期中很可能会发生两个变化。首先,需要添加新功能(Mary 已经在她的 backlog 中添加了这些功能)。其次,Mary 可能还需要应用Cross-Cutting Concerns。有了这些预期的变化,问题的明显答案是,“是的,当前的设计确实会导致彻底的变化。” 添加新功能和添加新方面时都会发生彻底的变化。

You can expect two quite likely changes to happen during the course of the lifetime of the e-commerce application. First, new features will need to be added (Mary already has them on her backlog). Second, Mary likely also needs to apply Cross-Cutting Concerns. With these expected changes, the obvious answer to the question is, “Yes, the current design does cause sweeping changes.” Sweeping changes happen both when adding new features and when adding new aspects.

当添加一个与产品相关的新功能时,更改会波及所有IProductService实现,这将是主要ProductService实现,以及所有装饰器和测试替身。添加新的横切关注点时,系统也可能会发生涟漪变化,因为除了添加新的 Decorator for 之外IProductService,您还将添加 Decorators for ICustomerService,IOrderService和所有其他I...Service 抽象。因为每个抽象可能包含许多方法,所以方面的代码会重复很多次,正如我们在 10.1 节中讨论的那样。

When a new product-related feature is added, the change ripples through all IProductService implementations, which will be the main ProductService implementation, and also all Decorators and Test Doubles. When a new Cross-Cutting Concern is added, there’ll likely be rippling changes to the system too, because, besides adding a new Decorator for IProductService, you’ll also be adding Decorators for ICustomerService, IOrderService, and all other I...Service Abstractions. Because each Abstraction potentially contains dozens of methods, the aspect’s code would be repeated many times, as we discussed in section 10.1.

在表 9.1 中,我们总结了您可能需要实施的各种可能方面。在项目开始时,您可能不知道需要哪些。但是,即使您可能不确切地知道您可能需要添加哪些横切关注点,假设您确实需要在项目过程中添加一些横切关注点是一个相当有根据的猜测,就像 Mary 所做的那样。

In table 9.1, we summed up a wide range of possible aspects you might need to implement. At the start of a project, you might not know which ones you’ll need. But even though you might not know exactly which Cross-Cutting Concerns you may need to add, it’d be a fairly well-educated guess to assume that you do need to add some during the course of the project, as Mary does.

结束我们的分析IProductService

Concluding our analysis of IProductService

从前面的分析中,您可以得出结论,清单 10.4及其实现违反了五个SOLID原则中的三个。尽管从 AOP 的角度来看,您可能会想使用动态拦截(第 11.1 节)或编译时织入工具(第 11.2 节)来应用切面,但我们认为这只能解决部分问题;即,如何以可维护的方式有效地应用横切关注点。使用工具并不能解决从长远来看仍然会导致可维护性问题的底层设计问题。

From the previous analysis, you can conclude that, together with its implementations, listing 10.4 violates three out of five SOLID principles. Although from the perspective of AOP, you might be tempted to use either dynamic Interception (section 11.1) or compile-time weaving tools (section 11.2) to apply aspects, we argue that this only solves part of the problem; namely, how to effectively apply Cross-Cutting Concerns in a maintainable fashion. The use of tools doesn’t fix the underlying design issues that still cause maintainability problems in the long run.

正如我们将在 11.1.2 和 11.2.2 节中讨论的那样,AOP 的两种方法都有它们自己特定的一组缺点。但是让我们看看我们是否可以通过 Mary 的应用程序获得更SOLID和可维护的设计。

As we’ll discuss in sections 11.1.2 and 11.2.2, both methods of AOP have their own particular sets of disadvantages. But let’s take a look at whether we can get to a more SOLID and maintainable design with Mary’s app.

10.3.3 通过应用SOLID原则改进设计

10.3.3 Improving design by applying SOLID principles

在本节中,我们将通过执行以下操作逐步改进应用程序的设计:

In this section, we’ll improve the application’s design step by step by doing the following:

  • 将读取与写入分开
  • Separate the reads from the writes
  • 通过拆分接口和实现来修复 ISP 和 SRP 违规
  • Fix the ISP and SRP violations by splitting interfaces and implementations
  • 通过引入参数对象和实现的通用接口来修复 OCP 违规
  • Fix the OCP violation by introducing Parameter Objects and a common interface for implementations
  • 通过定义通用抽象来修复意外引入的 LSP 违规
  • Fix the accidentally introduced LSP violation by defining a generic Abstraction

第 1 步:将读取与写入分开

Step 1: Separating reads from writes

Mary 当前设计的一个问题是应用到的大多数方面IProductService仅是其方法的一个子集所需要的。尽管安全性等方面通常适用于所有功能,但审计、验证和容错等方面通常只需要应用程序中更改状态的部分。另一方面,诸如缓存之类的方面可能仅对读取数据而不更改状态的方法有意义。IProductService您可以通过拆分为只读和只写接口来简化 Decorator 的创建,如图 10.1所示。

One of the problems with Mary’s current design is that the majority of aspects applied to IProductService are only required by a subset of its methods. Although an aspect such as security typically applies to all features, aspects such as auditing, validation, and fault tolerance will usually only be required around the parts of the application that change state. An aspect such as caching, on the other hand, may only make sense for methods that read data without changing state. You can simplify the creation of Decorators by splitting IProductService into a read-only and write-only interface, as shown in figure 10.1.

10-01.eps

图 10.1 分离IProductService为只读IProductQueryServices抽象和只写IProductCommandServices抽象

Figure 10.1 Separating IProductService into a read-only IProductQueryServicesAbstraction and a write-only IProductCommandServicesAbstraction

这种拆分的好处是新接口比以前更细粒度。这降低了您不得不依赖不需要的方法的风险。例如,当您创建一个将事务应用于已执行代码的装饰器时,只需要进行装饰,这样就无需实现的方法。它还使实现更小,更易于推理。IProductCommandServicesIProductQueryServices

The advantage of this split is that the new interfaces are finer-grained than before. This reduces the risk of you having to depend on methods that you don’t need. When you create a Decorator that applies a transaction to the executed code, for instance, only IProductCommandServices will need to be decorated, which eliminates the need to implement the IProductQueryServices’s methods. It also makes the implementations smaller and simpler to reason about.

尽管这种拆分是对原始IProductService界面的改进,但这种新设计仍然会带来翻天覆地的变化。和以前一样,实现与产品相关的新功能会导致应用程序中的许多类发生变化。尽管您将类被更改的可能性降低了一半,但更改仍然会导致大约相同数量的类被触及。这将我们带到了第二步。

Although this split is an improvement over the original IProductService interface, this new design still causes sweeping changes. As before, implementing a new product-related feature causes a change to many classes in the application. Although you reduced the likelihood of a class being changed by half, a change still causes about the same amount of classes to be touched. This brings us to the second step.

第 2 步:通过拆分接口和实现来修复 ISP 和 SRP

Step 2: Fixing ISP and SRP by splitting interfaces and implementations

因为拆分宽接口将我们推向了正确的方向,让我们更进一步。我们会集中注意力IProductCommandServices并忽略IProductQueryServices.

Because splitting the wide interface pushes us in the right direction, let’s take this a step further. We’ll focus our attention on IProductCommandServices and ignore IProductQueryServices.

让我们在这里尝试一些激进的东西。让我们分解IProductCommandServices成多个单成员接口。图 10.2显示了如何将ProductCommandServices实现分为七个类,每个类都有自己的单成员接口。

Let’s try something radical here. Let’s break up IProductCommandServices into multiple one-membered interfaces. Figure 10.2 shows how the ProductCommandServices implementation is segregated into seven classes, each with their own one-membered interface.

图 10.2中,您将接口的每个方法移动IProductCommandServices到一个单独的接口中,并为每个接口提供了自己的类。清单 10.6显示了其中的一些接口定义。

In figure 10.2 , you moved each method of the IProductCommandServices interface into a separate interface and gave each interface its own class. Listing 10.6 shows a few of those interface definitions.

10-02.eps

10.2IProductCommandServices界面包含七个成员的接口替换为七个单成员接口。每个接口都有自己对应的实现。

Figure 10.2 The IProductCommandServices interface containing seven members is replaced with seven, one-membered interfaces. Each interface gets its own corresponding implementation.

清单 10.6 分离成单成员接口的大接口

Listing 10.6 The big interface segregated into one-membered interfaces

public interface IAdjustInventoryService
{
    void AdjustInventory(Guid productId, bool decrease, int quantity);
}

public interface IUpdateProductReviewTotalsService
{
    void UpdateProductReviewTotals(Guid productId, ProductReview[] reviews);
}

public interface IUpdateHasDiscountsAppliedService
{
    void UpdateHasDiscountsApplied(Guid productId, string description);
}

...    ①  

这可能会把你吓得魂飞魄散,但它可能并不像看起来那么糟糕。以下是此更改的一些引人注目的优势:

This might scare the living daylights out of you, but it might not be as bad as it seems. Here are some compelling advantages to this change:

  • 每个接口都是隔离的。没有客户端会被迫依赖于它不使用的方法。
  • Every interface is segregated. No client will be forced to depend on methods it doesn’t use.
  • 当您创建从接口到实现的一对一映射时,应用程序中的每个用例都会获得自己的类。这使得班级变得小而集中——他们有单一的职责。
  • When you create a one-to-one mapping from interface to implementation, each use case in the application gets its own class. This makes classes small and focused — they have a single responsibility.
  • 添加新功能意味着添加新的接口实现对。无需对实现其他用例的现有类进行任何更改。
  • Adding a new feature means the addition of a new interface-implementation pair. No changes have to be made to existing classes that implement other use cases.

尽管这个新设计符合 ISP 和 SRP,但在创建装饰器时仍然会引起彻底的变化。这是如何做:

Even though this new design conforms to the ISP and the SRP, it still causes sweeping changes when it comes to creating Decorators. Here’s how:

  • IProductCommandServices接口拆分为七个单成员接口后,每个方面将有七个 Decorator 实现。例如,有 10 个方面,这意味着 70 个装饰器。
  • With the IProductCommandServices interface split into seven, one-membered interfaces, there’ll be seven Decorator implementations per aspect. With 10 aspects, for instance, this means 70 Decorators.
  • 对现有方面进行更改会导致对大量类的全面更改,因为每个方面都分布在许多装饰器上。
  • Making changes to an existing aspect causes sweeping changes throughout a large set of classes, because each aspect is spread out over many Decorators.

这种新设计使应用程序中的每个类都专注于一个特定的用例,从 SRP 和 ISP 的角度来看,这是很好的。但是,因为这些类没有可以应用方面的共性,所以您不得不创建许多具有几乎相同实现的装饰器。如果您能够为代码库中的所有命令操作定义一个接口,那就太好了。这将大大减少围绕方面的代码重复,并将 Decorator 类的数量减少到每个方面一个 Decorator。

This new design causes each class in the application to be focused around one particular use case, which is great from the perspective of the SRP and the ISP. But, because these classes have no commonality to which you can apply aspects, you’re forced to create many Decorators with almost identical implementations. It’d be nice if you were able to define a single interface for all command operations in the code base. That would greatly reduce the code duplication around aspects and the number of Decorator classes to one Decorator per aspect.

当您查看清单 10.6时,可能很难看出这些接口有何相似之处。它们都返回void,但都有一个不同命名的方法,并且每个方法都有一组不同的参数。没有可以从中提取的共性——或者有吗?

When you look at listing 10.6, it might be hard to see how these interfaces have any similarity. They all return void, but all have a differently named method, and each method has a different set of parameters. There’s no commonality to extract from that — or is there?

第 3 步:使用参数对象修复 OCP

Step 3: Fixing OCP using Parameter Objects

如果将每个命令方法的方法参数提取到一个Parameter Object中会怎样?大多数重构工具允许通过几个简单的击键进行这样的重构。

What if you extract the method parameters of each command method into a Parameter Object? Most refactoring tools allow such refactoring with a few simple keystrokes.

下一个清单显示了这次重构的结果。

The next listing shows the result of this refactoring.

清单 10.7 在参数对象中包装方法参数

Listing 10.7 Wrapping method parameters in a Parameter Object

public interface IAdjustInventoryService
{
    void Execute(AdjustInventory command);    ①  
}

public class AdjustInventory    ②  
{    ②  
    public Guid ProductId { get; set; }    ②  
    public bool Decrease { get; set; }    ②  
    public int Quantity { get; set; }    ②  
}    ②  

public interface IUpdateProductReviewTotalsService
{
    void Execute(UpdateProductReviewTotals command);  ③  
}

public class UpdateProductReviewTotals    ③  
{    ③  
    public Guid ProductId { get; set; }    ③  
    public ProductReview[] Reviews { get; set; }    ③  
}    ③  

重要的是要注意,即使Parameter ObjectsAdjustInventoryUpdateProductReviewTotalsParameter Objects 都是具体对象,它们仍然是Abstraction的一部分。正如我们在 3.1.1 节中提到的,因为它们只是没有行为的数据对象,将它们的值隐藏在抽象之后是毫无用处的。如果将实现移动到不同的程序集中,参数对象将与其抽象保持在同一个程序集中。此外,这些提取的参数对象成为命令操作的定义。因此,我们通常将这些对象本身称为命令

It’s important to note that even though both AdjustInventory and UpdateProductReviewTotals Parameter Objects are concrete objects, they’re still part of their Abstraction. As we mentioned in section 3.1.1, because they’re mere data objects without behavior, hiding their values behind an Abstraction would be rather useless. If you moved the implementations into a different assembly, the Parameter Objects would stay in the same assembly as their Abstraction. Also, these extracted Parameter Objects become the definition of a command operation. We therefore typically refer to these objects themselves as commands.

命令和命令都将有一个参数类型。但是,插入产品与更新产品的属性完全不同InsertProductUpdateHasTierPricesPropertyProductHasTierPrices. 同样,命令类型本身成为命令操作的定义。

Both the InsertProduct and UpdateHasTierPricesProperty commands will have a single parameter of type Product. Inserting a product, however, is something completely different than updating a product’s HasTierPrices property. Again, the command type itself becomes the definition of a command operation.

通过这些重构,您有效地将代码从 1 个接口和 7 个方法的实现更改为 7 个接口和 14 个类。在这一点上,您可能认为我们确实疯了,也许您已经准备好将这本书扔出窗外了。这可能就是我们在本节开头警告的心理不适。请耐心等待,因为增加系统中的类数量可能并不像一开始看起来那么糟糕,而且这种重构会让我们有所收获。承诺。

With these refactorings, you effectively changed the code from 1 interface and implementation with 7 methods, to 7 interfaces and 14 classes. At this point, you might think we’re certifiably nuts and perhaps you’re ready to toss this book out the window. This might be the mental discomfort we warned about at the beginning of this section. Bear with us, because increasing the number of classes in your system might not be as bad as it might seem at first, and this refactoring will get us somewhere. Promise.

通过前面的重构,出现了一种模式:

With the previous refactoring, a pattern emerges:

  • 每个抽象都包含一个方法。
  • Every Abstraction contains a single method.
  • 每个方法都被命名Execute
  • Every method is named Execute.
  • 每个方法都返回void
  • Every method returns void.
  • 每种方法都有一个输入参数。
  • Every method has one single input parameter.

您现在可以从此模式中提取一个通用接口。这是如何做:

You can now extract a common interface from this pattern. Here’s how:

public interface ICommandService    ①  
{
    void Execute(object command);
}

如果您使用这个新ICommandService接口实现命令服务,它产生清单 10.8中的代码。请注意,这个新的接口定义也可能用于替换其他I...Service 抽象

If you implement the command services using this new ICommandService interface, it results in the code in listing 10.8. Note that this new interface definition can likely be used to replace other I...Service Abstractions too.

清单 10.8 AdjustInventoryService实现ICommandService

Listing 10.8 AdjustInventoryService implementing ICommandService

public class AdjustInventoryService : ICommandService    ①  
{
    readonly IInventoryRepository repository;

    public AdjustInventoryService(    ②  
        IInventoryRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(object cmd)
    {
        var command = (AdjustInventory)cmd;    ③  

        Guid id = command.ProductId;    ④  
        bool decrease = command.Decrease;    ④  
        int quantity = command.Quantity;    ④  
        ...    ④  
    }
}

图 10.3显示了接口的数量是如何从 7 个减少到 1 个的。但是,现在您将方法参数提取到每个服务的参数对象中。

Figure 10.3 shows how the number of interfaces are reduced from seven back to one. Now, however, you extract the method parameters into a Parameter Object per service.

10-03.eps

图 10.3ICommandService通过将方法参数提取到 Parameter Objects 中,接口数量从 7 个减少到 1 个。

Figure 10.3 The number of interfaces is reduced from seven to one ICommandService by extracting method parameters into Parameter Objects.

正如我们之前所说,参数对象是抽象的一部分。将所有界面折叠成一个界面使这一点更加明显。参数对象已成为用例的定义——它已成为契约。消费者可以将其ICommandService注入到他们的构造函数中,并Execute通过提供适当的参数对象来调用其方法。

As we stated previously, the Parameter Objects are part of the Abstraction. Collapsing all interfaces into one single interface makes this even more apparent. The Parameter Object has become the definition of a use case — it has become the contract. Consumers can get this ICommandService injected into their constructor and call its Execute method by supplying the appropriate Parameter Object.

清单 10.9 InventoryController取决于ICommandService

Listing 10.9 InventoryController depending on ICommandService

public class InventoryController : Controller
{
    private readonly ICommandService service;

    public InventoryController(ICommandService service)  ①  
    {
        this.service = service;
    }

    [HttpPost]
    public ActionResult AdjustInventory(
        AdjustInventoryViewModel viewModel)
    {
        if (!this.ModelState.IsValid)
        {
            return this.View(viewModel);
        }

        AdjustInventory command = viewModel.Command;    ②  

        this.service.Execute(command);    ③  

        return this.RedirectToAction("Index");
    }
}

AdjustInventoryViewModel包装AdjustInventory命令_作为财产。这很方便,因为AdjustInventory它是抽象的一部分并且只包含特定于用例的数据。当用户回发请求时AdjustInventory,将由 MVC 框架及其周围的 进行模型绑定。AdjustInventoryViewModel

The AdjustInventoryViewModel wraps the AdjustInventory command as a property. This is convenient, because AdjustInventory is part of the Abstraction and only contains data specific to the use case. AdjustInventory will be model-bound by the MVC framework, together with its surrounding AdjustInventoryViewModel, when the user posts back the request.

ICommandService用于实施横切关注点

Using ICommandService to implement Cross-Cutting Concerns

在代码库中为所有命令服务调用提供一个单一接口提供了巨大的优势。因为应用程序的所有状态更改用例现在都实现了这个单一接口,所以您现在可以为每个方面创建一个装饰器并将其包装在每个实现周围。为了证明这一点,下面的清单显示了作为装饰器的事务方面的实现ICommandService

Having a single interface for all your command service calls in the code base provides a huge advantage. Because all the application’s state-changing use cases now implement this single interface, you can now create a single Decorator per aspect and wrap it around each and every implementation. To prove this point, the following listing shows the implementation of a transaction aspect as a Decorator for the ICommandService.

清单 10.10 基于ICommandService

Listing 10.10 Implementing a transaction aspect based on ICommandService

public class TransactionCommandServiceDecorator
    : ICommandService
{
    private readonly ICommandService decoratee;

    public TransactionCommandServiceDecorator(
        ICommandService decoratee)
    {
        this.decoratee = decoratee;
    }

    public void Execute(object command)
    {
        using (var scope = new TransactionScope())
        {
            this.decoratee.Execute(command);

            scope.Complete();
        }
    }
}

因为这个 Decorator 就像你在第 9 章多次看到的一样,我们认为它不需要解释,除了TransactionScope类之外。

Because this Decorator is like what you saw many times in chapter 9, we think it needs little explaining, except perhaps the TransactionScope class.

使用这个新的装饰器,您现在可以通过注入一个被a拦截的新装饰器来组合一个:InventoryControllerAdjustInventoryServiceTransaction-CommandServiceDecorator

Using this new Decorator, you can now compose an InventoryController by injecting a new AdjustInventoryService that gets Intercepted by a Transaction-CommandServiceDecorator:

ICommandService service =
    new TransactionCommandServiceDecorator(
        new AdjustInventoryService(repository));

new InventoryController(service);

这种设计有效地防止了在添加新功能和需要应用新的横切关注点时进行全面更改。此设计现在真正关闭修改,因为

This design effectively prevents sweeping changes both when new features are added and when new Cross-Cutting Concerns need to be applied. This design is now truly closed for modification because

  • 添加新的(命令)功能意味着创建新的命令参数对象和支持ICommandService实现。无需更改现有类。
  • Adding a new (command) feature means creating a new command Parameter Object and a supporting ICommandService implementation. No existing classes need to be changed.
  • 添加新功能不会强制创建新装饰器,也不会强制更改现有装饰器。
  • Adding a new feature doesn’t force the creation of new Decorators nor the change of existing Decorators.
  • 可以通过添加单个装饰器来向应用程序添加新的横切关注点。
  • Adding a new Cross-Cutting Concern to the application can be done by adding a single Decorator.
  • 更改横切关注点会导致更改单个类。
  • Changing a Cross-Cutting Concern results in changing a single class.

一些开发人员反对在他们的系统中使用这么多类,因为他们觉得这会使项目的导航变得复杂。然而,这只有在您没有正确构建项目时才会发生。在这个例子中,所有与产品相关的操作都可以放在一个名为 的命名空间中,有效地将这些操作组合在一起,类似于 Mary所做的。您现在可以在项目级别进行分组,而不是在类级别进行分组,这是一个很大的好处,因为项目结构会立即向您显示应用程序的行为。MyApp.Services.ProductsIProductService

Some developers argue against having this many classes in their system, because they feel it complicates navigating through the project. This, however, only happens when you don’t structure your project properly. In this example, all product-related operations can be placed in a namespace called MyApp.Services.Products, effectively grouping those operations together, similar to what Mary’s IProductService did. Instead of having the grouping at the class level, you now have it at the project level, which is a great benefit, because the project structure immediately shows you the application’s behavior.

现在您已经修复了之前分析的SOLID违规,您可能认为我们已经完成了重构。但是,不幸的是,这些更改意外地引入了新的SOLID违规行为。让我们接下来看看。

Now that you’ve fixed the previously analyzed SOLID violations, you might think that we’re done with our refactoring. But, unfortunately, these changes accidentally introduced a new SOLID violation. Let’s look at that next.

分析新的意外 LSP 违规

Analyzing the new accidental LSP violation

如前所述,ICommandService意外引入了新的SOLID违规的定义,即 LSP。InventoryController清单10.9的代码展示了这种违规行为。

As mentioned, the definition of ICommandService accidentally introduced a new SOLID violation, namely, the LSP. The InventoryController of listing 10.9 exhibits this violation.

正如我们在 10.2.3 节中讨论的那样,LSP 表示您必须能够在不改变客户端正确性的情况下用抽象替换同一抽象任意实现。根据 LSP,因为实现了,您应该能够在不破坏. 以下清单显示了 . 的更改对象组合。AdjustInventoryServiceICommandServiceInventoryControllerInventoryController

As we discussed in section 10.2.3, the LSP says that you must be able to substitute an Abstraction for an arbitrary implementation of that same Abstraction without changing the correctness of the client. According to the LSP, because the AdjustInventoryService implements the ICommandService, you should be able to substitute it for a different implementation without breaking the InventoryController. The following listing shows an altered object composition for InventoryController.

坏.tif

清单 10.11 替换AdjustInventoryService

Listing 10.11 Substituting AdjustInventoryService

ICommandService service =
    new TransactionCommandServiceDecorator(
        new UpdateProductReviewTotalsService(    ①  
            repository));

new InventoryController(service);

下面展示Execute方法对于:UpdateProductReviewTotalsService

The following shows the Execute method for UpdateProductReviewTotalsService:

public void Execute(object cmd)
{
    var command = (UpdateProductReviewTotals)cmd;    ①  
    ...
}

InventoryControllerICommandService注入到它的构造函数中。它传递AdjustInventory命令到那个注入ICommandService。因为注入ICommandService的是一个UpdateProductReviewTotalsService,它会尝试将传入的命令转换为UpdateProductReviewTotals。但是,因为它无法转换AdjustInventory为,所以转换失败。这会破坏并因此违反 LSP。UpdateProductReviewTotalsInventoryController

InventoryController gets an ICommandService injected into its constructor. It passes on the AdjustInventory command to that injected ICommandService. Because the injected ICommandService is an UpdateProductReviewTotalsService, it’ll try to cast the incoming command to UpdateProductReviewTotals. Because it’ll be unable to cast AdjustInventory to UpdateProductReviewTotals, however, the cast fails. This breaks InventoryController and therefore violates the LSP.

尽管有人可能会争辩说,由组合根来提供正确的实现,但该ICommandService接口仍然会导致歧义,并且它会阻止编译器验证我们的对象图的组合是否有意义。LSP 违规往往会使系统变得脆弱。此外,方法使用的无类型command方法参数Execute要求每个ICommandService实现都包含一个强制转换,这本身就可以被认为是一种代码味道。让我们解决这个违规问题。

Although one could argue that it’s up to the Composition Root to supply the correct implementation, the ICommandService interface still causes ambiguity, and it prevents the compiler from verifying whether the composition of our object graph makes sense. LSP violations tend to make a system fragile. Furthermore, the untyped command method argument that Execute methods consume requires every ICommandService implementation to contain a cast, which can be considered a code smell in its own right. Let’s fix this violation.

第 4 步:使用通用抽象修复 LSP

Step 4: Fixing LSP using a generic Abstraction

对于这个看似棘手的设计僵局,这里有一个相当优雅的解决方案。解决此问题所需要做的就是重新定义ICommandService

Here’s a rather elegant solution to this seemingly intractable design deadlock. All you have to do to fix this issue is redefine ICommandService.

好的.tif

清单 10.12 通用ICommandService实现

Listing 10.12 A generic ICommandService implementation

public interface ICommandService<TCommand>    ①  
{
    void Execute(TCommand command);
}

您可能对使界面通用化有何帮助感到困惑。为了帮助澄清这一点,下一个清单显示了您将如何实现ICommandService<TCommand>.

You might be confused as to how making the interface generic helps. To help clarify this, the next listing shows how you would implement ICommandService<TCommand>.

好的.tif

清单 10.13 实现AdjustInventoryServiceICommandService<TCommand>

Listing 10.13 AdjustInventoryService implementing ICommandService<TCommand>

public class AdjustInventoryService
    : ICommandService<AdjustInventory>    ①  
{
    private readonly IInventoryRepository repository;

    public AdjustInventoryService(
        IInventoryRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(AdjustInventory command)    ②  
    {
        var productId = command.ProductId;    ③  
    ③  
        ...    ③  
    }
}

许多框架和在线参考体系结构示例对类似于前面示例的接口有不同的名称。它们可能被命名为IHandler<T>ICommandHandler<T>IMessageHandler<T>IHandleMessages<T>。一些抽象是异步的并返回 a Task,而其他抽象则添加 aCancellationToken作为方法参数。有时该方法称为Handleor HandleAsync。尽管命名不同,但它的思想及其对应用程序可维护性的影响是相同的。

Many frameworks and online reference architecture samples have different names for an interface similar to the previous examples. They might be named IHandler<T>, ICommandHandler<T>, IMessageHandler<T>, or IHandleMessages<T>. Some Abstractions are asynchronous and return a Task, whereas others add a CancellationToken as a method argument. Sometimes the method is called Handle or HandleAsync. Although named differently, the idea and the effect it has on the maintainability of your application, however, is the same.

虽然在实现中额外的编译时支持当然是一个很好的优势,但泛型的主要原因ICommandService<TCommand>是为了防止在其客户端中违反 LSP。以下清单显示了如何注入ICommandService<TCommand>修复InventoryControllerLSP。

Although the additional compile-time support in the implementation is certainly a nice plus, the main reason for the generic ICommandService<TCommand> is to prevent violating the LSP in its clients. The following listing shows how injecting ICommandService<TCommand> into the InventoryController fixes the LSP.

好的.tif

清单 10.14 InventoryController取决于ICommandService<TCommand>

Listing 10.14 InventoryController depending on ICommandService<TCommand>

public class InventoryController : Controller
{
    readonly ICommandService<AdjustInventory> service;

    public InventoryController(
        ICommandService<AdjustInventory> service)    ①  
    {
        this.service = service;
    }

    public ActionResult AdjustInventory(
        AdjustInventoryViewModel viewModel)
    {
        ...

        AdjustInventory command = viewModel.Command;

        this.service.Execute(command);    ②  

        return this.RedirectToAction("Index");
    }
}

将非通用更改ICommandService为通用ICommandService<TCommand>修复了我们最后的SOLID违规。这将是收获我们新设计的好处的好时机。

Changing the non-generic ICommandService into the generic ICommandService<TCommand> fixes our last SOLID violation. This would be a good time to reap the benefits of our new design.

使用通用抽象应用事务处理

Applying transaction handling using the generic Abstraction

尽管通用的单成员抽象不仅仅是Cross-Cutting Concerns,但以不会导致彻底改变的方式应用方面的能力是这种设计的最大好处之一。与非通用ICommandService接口一样,ICommandService<TCommand>仍然允许为每个方面创建一个装饰器。清单 10.15显示了使用新的通用抽象重写清单 10.10的事务装饰器。ICommandService<TCommand>

Although there’s more to a generic one-membered Abstraction than just Cross-Cutting Concerns, the ability to apply aspects in a way that doesn’t cause sweeping changes is one of the greatest benefits of such a design. As with the non-generic ICommandService interface, ICommandService<TCommand> still allows the creation of a single Decorator per aspect. Listing 10.15 shows a rewrite of the transaction Decorator of listing 10.10 using the new generic ICommandService<TCommand> Abstraction.

好的.tif

清单 10.15 实现通用事务切面

Listing 10.15 Implementing a generic transaction aspect

public class TransactionCommandServiceDecorator<TCommand>
    : ICommandService<TCommand>
{
    private readonly ICommandService<TCommand> decoratee;

    public TransactionCommandServiceDecorator(
        ICommandService<TCommand> decoratee)
    {
        this.decoratee = decoratee;
    }

    public void Execute(TCommand command)
    {
        using (var scope = new TransactionScope())
        {
            this.decoratee.Execute(command);

            scope.Complete();
        }
    }
}

使用ICommandService<TCommand>界面TransactionCommandServiceDecorator<TCommand>装饰器,您的Composition Root变为以下内容:

Using the ICommandService<TCommand> interface and the TransactionCommandServiceDecorator<TCommand> Decorator, your Composition Root becomes the following:

new InventoryController(
    new TransactionCommandServiceDecorator<AdjustInventory>(
        new AdjustInventoryService(repository)));

这将我们带到了这个单成员泛型抽象开始抢尽风头的地步。这是您开始添加更多Cross-Cutting Concerns 的时候。

This brings us to the point where this one-membered generic Abstraction starts to steal the show. This is when you start adding more Cross-Cutting Concerns.

10.3.4 添加更多的横切关注点

10.3.4 Adding more Cross-Cutting Concerns

我们在 9.2 节中讨论的横切关注点的例子都集中在存储库边界的应用方面(例如清单 9.4 和 9.7)。然而,在本节中,我们将焦点转移到分层架构中的上一层,从数据访问库的存储库转移到域库的IProductService.

The examples of Cross-Cutting Concerns we discussed in section 9.2 all focused on applying aspects at the boundary of Repositories (such as in listings 9.4 and 9.7). In this section, however, we shift the focus one level up in the layered architecture, from the data access library’s repository to the domain library’s IProductService.

这种转变是有意为之的,因为您会发现存储库不是有效应用许多横切关注点的正确粒度级别。在域层中定义的单个业务操作可能会调用多个存储库,或者多次调用同一个存储库。例如,如果您要在存储库级别应用事务,这仍然意味着业务操作可能会在数十个事务中运行,这将危及系统的正确性。

This shift is deliberate, because you’ll find that Repositories aren’t the right granular level for applying many Cross-Cutting Concerns effectively. A single business action defined in the domain layer would potentially call multiple Repositories, or call the same Repository multiple times. If you were to apply, for instance, a transaction at the level of the repository, it’d still mean that the business operation could potentially run in dozens of transactions, which would endanger the correctness of the system.

单个业务操作通常应在单个事务中运行。这种粒度级别不仅适用于事务,也适用于其他类型的操作。

A single business operation should typically run in a single transaction. This level of granularity holds not only for transactions, but other types of operations as well.

域库实现业务操作,您通常希望在这个边界上应用许多Cross-Cutting Concerns。下面列举了一些例子。它不是一个全面的列表,但它会让您了解您可以在该级别上应用什么:

The domain library implements business operations, and it’s at this boundary that you typically want to apply many Cross-Cutting Concerns. The following lists some examples. It isn’t a comprehensive listing, but it’ll give you a sense of what you could apply on that level:

  • 审计——虽然你可以像清单 9.1 中所做的那样围绕存储库实施审计,但这会显示对单个实体的更改列表,你会失去整体情况——即为什么发生更改。报告对单个实体的更改可能适用于基于 CRUD 的应用程序,但如果应用程序实现更复杂的AuditingUserRepositoryDecorator影响多个实体的用例,将审计提升一个级别并存储有关已执行命令的信息变得有益。接下来我们将展示一个审计示例。
  • Auditing — Although you could implement auditing around Repositories, as you did in the AuditingUserRepositoryDecorator of listing 9.1, this presents a list of changes to individual Entities, and you lose the overall picture — that is, why the change happened. Reporting changes to individual Entities might be suited for CRUD-based applications, but if the application implements more-complex use cases that influence more than a single Entity, it becomes beneficial to pull auditing a level up and store information about the executed command. We’ll show an auditing example next.
  • 记录— 正如我们在 5.3.2 节中提到的,良好的应用程序设计可以防止不必要的日志语句遍布整个代码库。记录任何已执行的业务操作及其数据可为您提供有关调用的详细信息,这通常无需在每个方法开始时进行记录。
  • Logging — As we alluded to in section 5.3.2, a good application design can prevent unnecessary logging statements spread across the entire code base. Logging any executed business operation with its data provides you with detailed information about the call, which typically removes the need to log at the start of each method.
  • 性能监控— 由于执行请求的 99% 的时间通常用于运行业务操作本身,因此ICommandService<TCommand>成为插入性能监控的理想边界。
  • Performance monitoring — Since 99% of the time executing a request is typically spent running the business operation itself, ICommandService<TCommand> becomes an ideal boundary for plugging in performance monitoring.
  • 安全— 尽管您可能会尝试限制存储库级别的访问,但这通常过于细化,因为您更有可能希望在业务操作级别限制访问。您可以使用允许的角色或许可来标记您的命令,这使得使用单个装饰器将安全问题应用于所有业务操作变得微不足道。我们将很快展示一个例子。
  • Security — Although you might try to restrict access on the level of the repository, this is typically too fine-grained, because you more likely want to restrict access at the level of the business operation. You can mark your commands with either a permitted role or a permission, which makes it trivial to apply security concerns around all business operations using a single Decorator. We’ll show an example shortly.
  • 容错性— 因为你想围绕你的业务操作应用事务,正如我们在清单 10.15中所示,其他容错方面通常应该应用在同一级别上。例如,实现数据库死锁重试方面就是一个很好的例子。这种机制应该始终围绕事务方面应用。
  • Fault tolerance — Because you want to apply transactions around your business operations, as we’ve shown in listing 10.15, other fault-tolerant aspects should typically be applied on the same level. Implementing a database deadlock retry aspect, for instance, is a good example. Such a mechanism should always be applied around a transaction aspect.
  • 验证— 正如我们在代码清单 10.9 和 10.14 中演示的那样,该命令可以成为 Web 请求提交数据的一部分。通过使用数据注释的属性丰富命令,命令的数据也将由 MVC 验证。9  作为一项额外的安全措施,您可以创建一个装饰器,使用数据注释的静态Validator类来验证传入的命令。10 
  • Validation — As we demonstrated in listings 10.9 and 10.14, the command can become part of the web request’s submitted data. By enriching commands with Data Annotations’ attributes, the command’s data will also be validated by MVC.9  As an extra safety measure, you can create a Decorator that validates an incoming command using Data Annotations’ static Validator class.10 

以下部分将介绍如何在ICommandService<TCommand>.

The following sections take a look at how you can implement two of these aspects on top of ICommandService<TCommand>.

示例:实施审计方面

Example: Implementing an auditing aspect

清单 9.1 和 9.2 为 定义了一个审计装饰器IUserRepository,同时重用了IAuditTrailAppender清单 6.23 中的。相反,如果您应用审计ICommandService<TCommand>,则您处于理想的粒度级别,因为该命令包含您可能想要记录的所有有趣的用例特定数据。如果您使用一些上下文信息(例如用户名和当前系统时间)来丰富此数据和元数据,那么您就大功告成了。下一个清单显示了一个位于ICommandService<TCommand>.

Listings 9.1 and 9.2 defined an auditing Decorator for IUserRepository, while reusing the IAuditTrailAppender from listing 6.23. If you apply auditing on ICommandService<TCommand> instead, you’re at the ideal level of granularity, because the command contains all interesting use case–specific data you might want to record. If you enrich this data and metadata with some contextual information, such as username and the current system time, you’re pretty much done. The next listing shows an auditing Decorator on top of ICommandService<TCommand>.

好的.tif

清单 10.16 为业务运营实现一个通用的审计方面

Listing 10.16 Implementing a generic auditing aspect for business operations

public class AuditingCommandServiceDecorator<TCommand>
    : ICommandService<TCommand>
{
    private readonly IUserContext userContext;
    private readonly ITimeProvider timeProvider;
    private readonly CommerceContext context;
    private readonly ICommandService<TCommand> decoratee;

    public AuditingCommandServiceDecorator(
        IUserContext userContext,
        ITimeProvider timeProvider,    ①  
        CommerceContext context,
        ICommandService<TCommand> decoratee)
    {
        this.userContext = userContext;
        this.timeProvider = timeProvider;
        this.context = context;
        this.decoratee = decoratee;
    }

    public void Execute(TCommand command)
    {
        this.decoratee.Execute(command);
        this.AppendToAuditTrail(command);
    }

    private void AppendToAuditTrail(TCommand command)
    {
        var entry = new AuditEntry    ②  
        {    ②  
            UserId = this.userContext.CurrentUser.Id,    ②  
            TimeOfExecution = this.timeProvider.Now,    ②  
            Operation = command.GetType().Name,    ②  
            Data = Newtonsoft.Json.JsonConvert    ②  
                .SerializeObject(command)    ②  
        };

        this.context.AuditEntries.Add(entry);
        this.context.SaveChanges();
    }
}

当 Mary 使用 运行应用程序时,装饰器会在审计表中生成信息,如表 10.2所示。AuditingCommandServiceDecorator<TCommand>

When Mary runs the application using the AuditingCommandServiceDecorator<TCommand>, the Decorator produces the information in the auditing table, shown in table 10.2.

表 10.2 示例审计跟踪
用户时间手术数据
玛丽2018-12-24 11:20调整库存{ ProductId: "ae361...00bc", Decrease: false, Quantity: 2 }
玛丽2018-12-24 11:21UpdateHasTierPrices 属性{ 产品:{ Id:“ae361...00bc”,名称:“Gruyère”,单价:48.50,IsFeatured:true } }
玛丽2018-12-24 11:25UpdateHasDiscountsApplied{ ProductId: "ae361...00bc", DiscountDescription: "Test" }
玛丽2018-12-24 15:11调整库存{ ProductId: "5435...a845", Decrease: true, Quantity: 1 }
玛丽2018-12-24 15:12更新ProductReviewTotals{ ProductId:“5435...a845”,评论:[{ 评分:5,文字:“不错!” }] }

如前所述,AuditingCommandServiceDecorator<TCommand>使用反射获取命令的名称并将命令转换为 JSON 格式。尽管 JSON 是人类可读的,但您可能不希望将其显示给最终用户。不过,这是一种用于后端审计目的的好格式。使用此信息,您将能够有效地查看系统中发生了什么、由谁以及在什么时间点发生的。它甚至允许您在操作因某种原因失败时重播该操作,或者使用此信息对系统执行真实的压力测试。您可以将此表中的信息反序列化回命令并在系统中运行它们。

As stated previously, AuditingCommandServiceDecorator<TCommand> uses reflection to get the name of the command and convert the command to a JSON format. Although JSON is human readable, you probably don’t want to show this to your end users. Still, this is a good format to use for backend auditing purposes. Using this information, you’ll be able to efficiently see what happened in your system, by whom, and at which point in time. It would even allow you to replay an operation if it failed for some reason or to use this information to perform a realistic stress test on the system. You could deserialize the information from this table back to commands and run them through the system.

正如我们在 6.3.2 节中描述的,域事件是另一种非常适合的技术,也可以用于审计。然而,该审核方面仅记录用户的成功操作。尽管审计员可能对失败不感兴趣,但我们作为开发人员肯定感兴趣。不难想象您将如何使用相同的机制来记录相同的数据并在操作失败时包含堆栈跟踪。

As we described in section 6.3.2, domain events are another well-suited technique that can also be used for auditing. This auditing aspect, however, only records a user’s successful action. Although an auditor might not be interested in failures, we as developers certainly are. It isn’t hard to imagine how you’d use the same mechanism to record the same data and include a stack trace when the operation fails.

同样,您可以使用此信息以相同的方式进行性能监控,您可以在时间和操作详细信息旁边存储一个额外的时间跨度。这很容易让您监控哪些操作随时间变慢。在向您展示应用了新组合根的示例之前AuditingCommandServiceDecorator<TCommand>,我们将首先了解如何使用被动属性来实现安全方面。

Likewise, you can use this information for performance monitoring in the same way, where you store an additional timespan next to the time and the operation details. This easily allows you to monitor which operations become slower over time. Before showing you an example of the new Composition Root with AuditingCommandServiceDecorator<TCommand> applied, we’ll first take a look at how you can use passive attributes to implement a security aspect.

示例:实现安全方面

Example: Implementing a security aspect

在9.2 节中关于横切关注点的讨论中,您在清单 9.7 中实现了一个。因为那个装饰器是特定于 的,所以很清楚装饰器应该授予什么角色访问权限。在示例中,对 的写入方法的访问仅限于角色。SecureProductRepositoryDecoratorIProductRepositoryIProductRepositoryAdministrator

During our discussion about Cross-Cutting Concerns in section 9.2, you implemented a SecureProductRepositoryDecorator in listing 9.7. Because that Decorator was specific to IProductRepository, it was clear what role the Decorator should grant access to. In the example, access to the write methods of IProductRepository was restricted to the Administrator role.

有了这个新的通用模型,一个装饰器就包裹了所有业务操作,而不仅仅是产品 CRUD 操作。一些操作还需要其他角色可以执行,这使得硬编码Administrator角色不适合这种通用模型。您可以通过多种方式在通用抽象之上实施此类安全检查,但一种引人注目的方法是使用被动属性。

With this new generic model, a single Decorator is wrapped around all business operations, not just the product CRUD operations. Some operations also need to be executable by other roles, which makes the hard-coded Administrator role unsuited for this generic model. You can implement such a security check on top of a generic Abstraction in many ways, but one compelling method is through the use of passive attributes.

当您坚持使用基于角色的安全性作为授权示例时,您可以指定一个PermittedRoleAttribute.

When you stick to role-based security as an example of authorization, you can specify a PermittedRoleAttribute.

清单 10.17 无源PermittedRoleAttribute

Listing 10.17 A passive PermittedRoleAttribute

public class PermittedRoleAttribute : Attribute  ①  
{
    public readonly Role Role;

    public PermittedRoleAttribute(Role role)    ②  
    {
        this.Role = role;
    }
}

public enum Role    ③  
{
    PreferredCustomer,
    Administrator,
    InventoryManager
}

您可以使用此属性通过有关允许哪个角色执行操作的元数据来丰富命令。

You can use this attribute to enrich commands with metadata about which role is allowed to execute an operation.

好的.tif

清单 10.18 使用与安全相关的元数据丰富命令

Listing 10.18 Enriching commands with security-related metadata

[PermittedRole(Role.InventoryManager)]    ①  
public class AdjustInventory
{
    public Guid ProductId { get; set; }
    public bool Decrease { get; set; }
    public int Quantity { get; set; }
}

[PermittedRole(Role.Administrator)]    ①  
public class UpdateProductReviewTotals
{
    public Guid ProductId { get; set; }
    public ProductReview[] Reviews { get; set; }
}

正如我们将在 11.2 节中讨论的那样,应用方面属性与被动属性(例如. 与方面属性相比,被动属性与使用它们值的方面解耦,这是编译时编织的主要问题之一,正如您将在第 11 章中看到的那样。被动属性与方面。这允许元数据被多个方面重用,也许以不同的方式。PermittedRoleAttribute

There’s a big difference between applying aspect attributes, as we’ll discuss in section 11.2, and a passive attribute, such as the PermittedRoleAttribute. Compared to aspect attributes, passive attributes are decoupled from the aspect that use their values, which is one of the main problems with compile-time weaving, as you’ll see in chapter 11. The passive attribute doesn’t have a direct relationship with the aspect. This allows the metadata to be reused by multiple aspects, perhaps in different ways.

正如您之前看到的那样,添加安全行为就是创建装饰器并将其包装在实际实现中。清单 10.19显示了这样一个装饰器。它使用提供给命令的 that,如清单 10.18所示。PermittedRoleAttribute

Like you’ve seen previously, adding the security behavior is a matter of creating the Decorator and wrapping it around the real implementation. Listing 10.19 shows such a Decorator. It makes use of the PermittedRoleAttribute that’s supplied to commands, as listing 10.18 showed.

好的.tif

清单 10.19 SecureCommandServiceDecorator<TCommand>

Listing 10.19 SecureCommandServiceDecorator<TCommand>

public class SecureCommandServiceDecorator<TCommand>
    : ICommandService<TCommand>
{
    private static readonly Role PermittedRole = GetPermittedRole();    ⑧  
 
    private readonly IUserContext userContext;
    private readonly ICommandService<TCommand> decoratee;
 
    public SecureCommandServiceDecorator(
        IUserContext userContext,    ①  
        ICommandService<TCommand> decoratee)
    {
        this.decoratee = decoratee;
        this.userContext = userContext;
    }

    public void Execute(TCommand command)
    {
        this.CheckAuthorization();    ②  
        this.decoratee.Execute(command);
    }

    private void CheckAuthorization()
    {
        if (!this.userContext.IsInRole(PermittedRole))    ④  
        {
            throw new SecurityException();
        }
    }

    private static Role GetPermittedRole()
    {
        var attribute = typeof(TCommand)    ⑤  
            .GetCustomAttribute<PermittedRoleAttribute>();

        if (attribute == null)
        {
            throw new InvalidOperationException(    ⑥  
                "[PermittedRole] missing.");    ⑥  
        }

        return attribute.Role;
    }
}

我们可以为您提供大量可以围绕业务交易包装的装饰器示例,但是一本书可以拥有的页数是有限制的。此外,在这一点上,我们认为您已经开始了解如何在 上应用装饰器ICommandService<TCommand>让我们将Composition Root中的所有内容拼凑在一起。

We could give you tons of examples of Decorators that can be wrapped around business transactions, but there’s a limit to the number of pages a book can have. Besides, at this point, we think you’re starting to get the picture about how to apply Decorators on top of ICommandService<TCommand>. Let’s piece everything together inside the Composition Root.

使用通用装饰器组合对象图

Composing object graphs using generic Decorators

10-04.eps

图 10.4 用审计、事务和安全方面丰富一个真正的命令服务

Figure 10.4 Enriching a real command service with auditing, transaction, and security aspects

在前面的部分中,您声明了三个实现安全、事务管理和审计的装饰器。您需要围绕Composition Root中的真实实现应用这些装饰器。图 10.4显示了装饰器如何像一组俄罗斯套娃一样包裹在命令服务周围。

In the previous sections, you declared three Decorators implementing security, transaction management, and auditing. You need to apply these Decorators around a real implementation in your Composition Root. Figure 10.4 shows how the Decorators are wrapped around a command service like a set of Russian nesting dolls.

如果您将所有三个先前定义的装饰器应用到您的Composition Root,您最终会得到下面显示的代码。

If you apply all three previously defined Decorators to your Composition Root, you end up with the code shown next.

清单 10.20 装饰AdjustInventoryService

Listing 10.20 Decorating AdjustInventoryService

ICommandService<AdjustInventory> service =
    new SecureCommandServiceDecorator<AdjustInventory>(
        this.userContext,
        new TransactionCommandServiceDecorator<AdjustInventory>(
            new AuditingCommandServiceDecorator<AdjustInventory>(
                this.userContext,
                this.timeProvider,
                context,
                new AdjustInventoryService(repository))));

return new InventoryController(service);

因为应用程序预计会获得许多ICommandService<TCommand>实现,所以大多数实现都需要相同的装饰器。因此,清单 10.20会导致Composition Root中出现大量代码重复。通过将重复的 Decorator 创建提取到它自己的方法中,可以很容易地解决这个问题。

Because the application is expected to get many ICommandService<TCommand> implementations, most of the implementations would require the same decorators. Listing 10.20, therefore, would lead to lots of code repetition inside the Composition Root. This is something that’s easily fixed by extracting the repeated Decorator creation into its own method.

清单 10.21 将装饰器的组合提取到可重用的方法中

Listing 10.21 Extracting the composition of Decorators to a reusable method

private ICommandService<TCommand> Decorate<TCommand>(
    ICommandService<TCommand> decoratee, CommerceContext context)
{
    return
        new SecureCommandServiceDecorator<TCommand>(
            this.userContext,
            new TransactionCommandServiceDecorator<TCommand>(
                AuditingCommandServiceDecorator<TCommand>(
                    this.userContext,
                    this.timeProvider,
                    context,
                    decoratee))));    ①  
}

将 Decorators 提取到Decorate方法中允许Composition Root完全 DRY。的创建被简化为一个简单的单行代码:AdjustInventoryService

Extracting the Decorators into the Decorate method allows the Composition Root to be completely DRY. The creation of AdjustInventoryService is reduced to a simple one-liner:

var service = Decorate(new AdjustInventoryService(repository), context);

return new InventoryController(service);

第 12 章演示了如何使用DI 容器自动注册 实现和应用装饰器。因为这几乎让我们结束了关于使用SOLID原则作为 AOP 驱动程序的部分,让我们反思一下我们已经取得的成就以及这与应用程序设计的大局有何关系。ICommandService<TCommand>

Chapter 12 demonstrates how to Auto-Register ICommandService<TCommand> implementations and apply Decorators using a DI Container. Because this almost brings us to the end of this section about using SOLID principles as a driver for AOP, let’s reflect for a moment on what we’ve achieved and how this relates to the bigger picture of application design.

10.3.5 结论

10.3.5 Conclusion

在本章中,您将IProductService包含多个命令方法的领域层的 big 重构为单个ICommandService<TCommand> 抽象,其中每个命令都有自己的消息和用于处理该消息的相关实现。这种重构并没有改变任何原始的应用程序逻辑;但是,您确实明确了命令的概念。

In this chapter, you refactored the domain layer’s big IProductService, which consisted of several command methods, into a single ICommandService<TCommand> Abstraction, where each command got its own message and associated implementation for handling that message. This refactoring didn’t change any of the original application logic; you did, however, make the concept of commands explicit.

一个重要的观察是,这些域命令现在作为系统中的一个明确的工件公开,并且它们的处理程序被标记为单一接口。这种方法类似于您在使用 ASP.NET Core MVC 等应用程序框架时隐式练习的方法。MVC 控制器通常是通过继承Controller 抽象来定义的;这允许 MVC 使用反射找到它们,并且它提供了一个用于与它们交互的通用 API。这种做法在应用程序设计中的更大范围内是有价值的,正如您在这些命令中看到的那样,您在这些命令中为它们的处理程序提供了一个通用 API(单一Execute方法)。这允许有效地应用方面并且没有代码重复。

An important observation is that these domain commands are now exposed as a clear artifact in the system, and their handlers are marked with a single interface. This methodology is similar to what you implicitly practice when working with application frameworks such as ASP.NET Core MVC. MVC Controllers are typically defined by inheriting from the Controller Abstraction; this allows MVC to find them using reflection, and it presents a common API for interacting with them. This practice is valuable at a larger scale in the application’s design, as you’ve seen with these commands where you gave their handlers a common API (a single Execute method). This allowed aspects to be applied effectively and without code repetition.

除了命令之外,系统中还有其他工件,您可能希望以类似的方式设计这些工件,以便能够应用Cross-Cutting Concerns。一个值得更清楚地公开的常见工件是查询。在 10.3.3 节的开头,当你拆分IProductService成一个读写接口后,我们将你的注意力集中在了 上,而忽略了。查询应该有自己的抽象。然而,由于篇幅所限,对此的讨论超出了本书的范围。15IProductCommandServicesIProductQueryServices 

Besides commands, there are other artifacts in the system that you might want to design in a similar fashion in order to be able to apply Cross-Cutting Concerns. A common artifact that deserves to be exposed more clearly is that of a query. At the start of section 10.3.3, after you split up IProductService into a read and write interface, we focused your attention on IProductCommandServices and ignored IProductQueryServices. Queries deserve an Abstraction of their own. Due to space constraints, however, a discussion of this is outside the scope of this book.15 

然而,我们的观点是,在许多类型的应用程序中,可以像本章中所做的那样确定相关组件组之间的共性。这可能有助于更有效地应用Cross-Cutting Concerns,并为您提供明确的和编译器验证的编码约定。

Our point, however, is that in many types of applications, it’s possible to determine a commonality between groups of related components as you did in this chapter. This might help with applying Cross-Cutting Concerns more effectively and also supplies you with an explicit and compiler-verified coding convention.

但本章的目的并不是要说明ICommandService<TCommand> 抽象是设计应用程序的方式。本章的重要收获应该是,根据SOLID设计应用程序是保持应用程序可维护的方法。正如我们所展示的,在大多数情况下,这可以在不使用专门的 AOP 工具的情况下实现。这一点很重要,因为这些工具有其自身的局限性和问题,我们将在下一章深入探讨。然而,我们发现了一组特定的设计结构适用于许多业务线 (LOB) 应用程序——ICommandService抽象就是其中之一。

But the goal of this chapter wasn’t to state that the ICommandService<TCommand> Abstraction is the way to design your applications. The important takeaway from this chapter should be that designing applications according to SOLID is the way to keep applications maintainable. As we demonstrated, this can, for the most part, be achieved without the use of specialized AOP tooling. This is important, because those tools come with their own sets of limitations and problems, which is something we’ll go into deeper in the next chapter. We have found, however, a certain set of design structures to be applicable to many line-of-business (LOB) applications — an ICommandService-like Abstraction being one of them.

这并不意味着应用SOLID原则总是很容易。相反,这可能很困难。如前所述,这需要时间,而且您永远不会 100%可靠。作为软件开发人员,您的工作就是找到最佳点;在适当的时候应用 DI 和SOLID绝对会增加你接近它的机会。

This doesn’t mean that it’s always easy to apply SOLID principles. On the contrary, it can be difficult. As stated previously, it takes time, and you’ll never be 100% SOLID. Your job as software developers is to find the sweet spot; applying DI and SOLID at the right moments will absolutely boost your chances of getting closer to that.

在应用公认的面向对象原则(如SOLID )时,DI 大放异彩。特别是,DI 的松散耦合特性使您可以使用装饰器模式来遵循 OCP 和 SRP。这在很多情况下都很有价值,因为它使您能够保持代码的整洁和组织良好,尤其是在解决Cross-Cutting Concerns时。

DI shines when it comes to applying recognized object-oriented principles such as SOLID. In particular, the loosely coupled nature of DI lets you use the Decorator pattern to follow the OCP as well as the SRP. This is valuable in a wide range of situations, because it enables you to keep your code clean and well organized, especially when it comes to addressing Cross-Cutting Concerns.

但我们不要拐弯抹角。编写可维护的软件很难,即使您尝试应用SOLID原则。此外,您经常从事经不起时间考验的项目。进行大的架构更改可能是不可行或危险的。在那些时候,使用 AOP 工具可能是您唯一可行的选择,即使它为您提供了一个临时解决方案。在您决定使用这些工具之前,了解它们的工作原理以及它们的弱点非常重要,尤其是与本章中描述的设计理念相比。这将是下一章的主题。

But let’s not beat around the bush. Writing maintainable software is hard, even when you try to apply the SOLID principles. Besides, you often work in projects that aren’t designed to stand the test of time. It might be unfeasible or dangerous to make big architectural changes. At those times, using AOP tooling might be your only viable option, even if it presents you with a temporary solution. Before you decide to use these tools, it’s important to understand how they work and what their weaknesses are, especially compared to the design philosophy described in this chapter. This will be the subject of the next chapter.

概括

Summary

  • 单一职责原则( SRP) 指出每个类应该只有一个更改原因。这可以从凝聚力的角度来看。内聚被定义为类或模块的元素的功能相关性。相关性越低,凝聚力越低;凝聚力越低,类违反 SRP 的可能性就越大。
  • The Single Responsibility Principle (SRP) states that each class should have only one reason to change. This can be viewed from the perspective of cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the amount of relatedness, the lower the cohesion; and the lower the cohesion, the greater the chance a class violates the SRP.
  • 开放/封闭原则(OCP) 规定了一种应用程序设计,使您不必对整个代码库进行彻底的更改。OCP 和 DRY 原则之间的密切关系是它们都为同一个目标而努力。
  • The Open/Closed Principle (OCP) prescribes an application design that prevents you from having to make sweeping changes throughout the code base. A strong relationship between the OCP and the DRY principle is that they both strive for the same objective.
  • 不要重复自己 (DRY) 原则指出,每条知识都必须在系统中具有单一、明确、权威的表示形式。
  • The Don’t Repeat Yourself (DRY) principle states that every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
  • Liskov 替换原则(LSP) 指出每个实现都应按照其抽象定义的方式运行。这使您可以用相同抽象的另一个实现替换最初预期的实现,而不必担心破坏消费者。这是DI的基础。当消费者不观察它时,注入Dependencies几乎没有优势,因为您不能随意替换Dependencies,并且您会失去 DI 的许多(如果不是全部)好处。
  • The Liskov Substitution Principle (LSP) states that every implementation should behave as defined by its Abstraction. This lets you replace the originally intended implementation with another implementation of the same Abstraction without worrying about breaking a consumer. It’s a foundation of DI. When consumers don’t observe it, there’s little advantage in injecting Dependencies, because you can’t replace Dependencies at will, and you lose many (if not all) benefits of DI.
  • 接口隔离原则(ISP) 促进使用细粒度抽象而不是宽抽象。每当消费者依赖于一些成员未被使用的抽象时,就会违反此原则。当涉及到有效应用面向方面的编程时,这个原则是至关重要的。
  • The Interface Segregation Principle (ISP) promotes the use of fine-grained Abstractions rather than wide Abstractions. Any time a consumer depends on an Abstraction where some of the members stay unused, this principle is violated. This principle is crucial when it comes to effectively applying Aspect-Oriented Programming.
  • 依赖倒置原则(DIP) 指出您应该针对抽象进行编程,并且消费层应该控制消费抽象的形状。消费者应该能够以最有利于自己的方式定义抽象
  • The Dependency Inversion Principle (DIP) states that you should program against Abstractions and that the consuming layer should be in control of the shape of a consumed Abstraction. The consumer should be able to define the Abstraction in a way that benefits itself the most.
  • 这五个原则共同构成了SOLID的首字母缩写词。SOLID原则中没有一个是绝对的。它们是可以帮助您编写干净代码的指南。
  • These five principles together form the SOLID acronym. None of the SOLID principles represents absolutes. They’re guidelines that can help you write clean code.
  • 面向方面的编程(AOP) 是一种范例,它侧重于有效且可维护地应用横切关注点的概念。
  • Aspect-Oriented Programming (AOP) is a paradigm that focuses on the notion of applying Cross-Cutting Concerns effectively and maintainably.
  • 最引人注目的 AOP 技术是SOLIDSOLID应用程序可防止在正常应用程序代码和Cross-Cutting Concerns实现期间出现代码重复。使用SOLID技术还可以帮助开发人员避免使用特定的 AOP 工具。
  • The most compelling AOP technique is SOLID. A SOLID application prevents code duplication during normal application code and implementation of Cross-Cutting Concerns. Using SOLID techniques can also help developers avoid the use of specific AOP tooling.
  • 即使采用SOLID设计,也可能会出现彻底改变的时刻。100% 关闭修改既不可能也不可取。在寻找和设计合适的抽象时,遵循 OCP 需要付出相当大的努力,尽管过多的抽象会对应用程序的复杂性产生负面影响。
  • Even with a SOLID design, there likely will come a time where a change becomes sweeping. Being 100% closed for modification is neither possible nor desirable. Conforming to the OCP takes considerable effort when finding and designing the appropriate Abstractions, although too many Abstractions can have a negative impact on the complexity of the application.
  • 命令查询分离(CQS) 是一个有影响力的面向对象原则,它指出每个方法要么返回结果但不改变系统的可观察状态,要么改变状态但不产生任何值。
  • Command-Query Separation (CQS) is an influential object-oriented principle that states that each method should either return a result but not change the observable state of the system, or change the state but not produce any value.
  • 将命令方法和查询方法放在不同的抽象中可以简化应用Cross-Cutting Concerns,因为大多数方面需要应用于命令或查询,而不是两者。
  • Placing command methods and query methods in different Abstractions simplifies applying Cross-Cutting Concerns, because the majority of aspects need to be applied to either commands or queries, but not both.
  • 参数对象是一组自然组合在一起的参数。参数对象的提取允许定义可由大量组件实现的可重用抽象。这允许以类似方式处理这些组件,并有效地应用跨领域关注点
  • A Parameter Object is a group of parameters that naturally go together. The extraction of Parameter Objects allows the definition of a reusable Abstraction that can be implemented by a large group of components. This allows these components to be handled similarly and Cross-Cutting Concerns to be applied effectively.
  • 这些提取的参数对象不是组件的抽象,而是成为系统中不同操作或用例的定义。
  • Rather than a component’s Abstraction, these extracted Parameter Objects become the definition of a distinct operation or use case in the system.
  • 尽管使用参数对象将较大的类拆分为许多较小的类可以显着增加系统中类的数量,但它也可以显着提高系统的可维护性。系统中类的数量是衡量可维护性的一个不好的指标。
  • Although splitting larger classes into many smaller classes with Parameter Objects can drastically increase the number of classes in a system, it can also dramatically improve the maintainability of a system. The number of classes in a system is a bad metric for measuring maintainability.
  • 横切关注点应该在应用程序中以正确的粒度级别应用。对于除最简单的 CRUD 应用程序之外的所有应用程序,存储库不是大多数横切关注点的正确粒度级别。随着SOLID原则的应用,可重用的单成员抽象通常作为需要应用横切关注点的级别出现。
  • Cross-Cutting Concerns should be applied at the right granular level in the application. For all but the simplest CRUD applications, Repositories aren’t the right granular level for most Cross-Cutting Concerns. With the application of SOLID principles, reusable one-membered Abstractions typically emerge as the levels where Cross-Cutting Concerns need to be applied.

第11

章 基于工具的面向切面编程

11

Tool-based Aspect-Oriented Programming

在这一章当中

In this chapter

  • 使用动态拦截通过生成的装饰器应用拦截器
  • Using dynamic Interception to apply Interceptors using generated Decorators
  • 动态拦截的优缺点
  • Advantages and disadvantages of dynamic Interception
  • 使用编译时编织来应用横切关注点
  • Using compile-time weaving to apply Cross-Cutting Concerns
  • 为什么编译时编织是 DI 的对立面
  • Why compile-time weaving is the antithesis of DI

本章是我们在第 10 章开始的面向方面编程 (AOP) 讨论的延续。第 10 章以最纯粹的形式描述了 AOP——即,仅使用SOLID设计实践来应用 AOP——本章从工具——基于的观点。我们将讨论应用 AOP 的两种常用方法:动态拦截和编译时织入。

This chapter is a continuation of the Aspect-Oriented Programming (AOP) discussion that we started in chapter 10. Where chapter 10 described AOP in its purest form — namely, applying AOP solely using SOLID design practices — this chapter approaches AOP from a tool-based perspective. We’ll discuss two common methods for applying AOP: dynamic Interception and compile-time weaving.

如果第 10 章的设计方法过于激进,动态拦截将是您的下一个最佳选择,这就是我们首先讨论它的原因。在开始进行上一章讨论的各种改进之前,动态拦截可能是一个很好的临时解决方案。

In case the design approach of chapter 10 is too radical, dynamic Interception will be your next best pick, which is why we’ll discuss it first. Dynamic Interception might be a good temporary solution until the right time arrives to start making the kinds of improvements discussed in the last chapter.

编译时编织与 DI 相反,我们认为它是一种反模式。然而,我们认为包含关于编译时织入的讨论很重要,因为它是一种众所周知的 AOP 形式,而且我们想明确表示它不是 DI 的可行替代方案。

Compile-time weaving is the opposite of DI, and we consider it to be an anti-pattern. We feel it’s important, however, to include a discussion on compile-time weaving, because it’s a well-known form of AOP, and we want to make it clear that it isn’t a viable alternative to DI.

11.1 动态拦截

11.1 Dynamic Interception

10.1 节的代码清单实现了 的DeleteInsert方法,包含代码重复。下面的清单再次显示了这段代码。CircuitBreakerProductRepositoryDecorator

The code listings of section 10.1, which implement the Delete and Insert methods of CircuitBreakerProductRepositoryDecorator, contained code duplication. The following listing shows this code again.

气味.tif

清单 11.1 违反 DRY原则(重复)

Listing 11.1 Violating the DRY principle (repeated)

public void Delete(Product product)
{
    this.breaker.Guard();    ①  
    ①  
    try    ①  
    {    ①  
        this.decoratee.Delete(product);    ①  
        this.breaker.Succeed();    ①  
    }    ①  
    catch (Exception ex)    ①  
    {    ①  
        this.breaker.Trip(ex);    ①  
        throw;    ①  
    }    ①  
}

public void Insert(Product product)
{
    this.breaker.Guard();    ①  
    ①  
    try    ①  
    {    ①  
        this.decoratee.Insert(product);    ①  
        this.breaker.Succeed();    ①  
    }    ①  
    catch (Exception ex)    ①  
    {    ①  
        this.breaker.Trip(ex);    ①  
        throw;    ①  
    }    ①  
}

将 Decorator 作为方面实现的最困难的部分是设计模板。之后,这是一个相当机械的过程:

The hardest part of implementing a Decorator as an aspect is to design the template. After that, it’s a rather mechanical process:

  1. 新建装饰器类
  2. Create a new Decorator class
  3. 从所需的界面派生
  4. Derive from the desired interface
  5. 通过应用模板实现每个接口成员
  6. Implement each interface member by applying the template

这个过程非常重复,您可以使用工具来自动化它。.NET Framework 的众多强大功能之一是能够动态发出类型。这使得编写在运行时生成功能齐全的类的代码成为可能。这样的类没有底层源代码文件,而是直接从一些抽象模型编译而来。这使您能够自动生成在运行时创建的装饰器。如图11.1所示,这就是动态拦截使您能够做到的。

This process is so repetitive that you can use a tool to automate it. Among the many powerful features of the .NET Framework is the ability to dynamically emit types. This makes it possible to write code that generates a fully functional class at runtime. Such a class has no underlying source code file, but is compiled directly from some abstract model. This enables you to automate the generation of Decorators that are created at runtime. As figure 11.1 shows, this is what dynamic Interception enables you to do.

动态生成的Decorator及其Dependencies的对象图创建完成后,Decorator就可以作为真实类的替身。因为它实现了真实类的Abstraction,所以它可以被注入到使用该Abstraction的客户端中。图 11.2描述了客户端调用其拦截抽象时方法调用的流程。

After the object graph for the dynamically generated Decorator and its Dependencies is created, the Decorator can be used as a stand-in for the real class. Because it implements the real class’s Abstraction, it can be injected into clients that use that Abstraction. Figure 11.2 describes the flow of method calls when the client calls into its Intercepted Abstraction.

11-01.eps

图 11.1 动态拦截库在运行时生成装饰器类。每个给定的抽象都会发生一次(在这种情况下,对于IRepo)。生成过程完成后,您可以请求拦截库为您创建该装饰器的新实例,同时您提供目标拦截器

Figure 11.1 A dynamic Interception library generates a Decorator class at runtime. This happens once per given Abstraction (in this case, for IRepo). After the generation process completes, you can request that the Interception library create new instances of that Decorator for you, while you supply both the target and the interceptor.

11-02.eps

图 11.2 客户端调用其拦截抽象时的方法调用流程

Figure 11.2 The flow of method calls when the client calls into its Intercepted Abstraction

要使用动态拦截,您仍然必须编写实现方面的代码。这可能是断路器方面所需的管道代码,因为清单 11.1所示。一旦你完成了这个,你必须告诉动态拦截库它应该应用这个方面的抽象。足够的理论,让我们看一个例子。

To use dynamic Interception, you must still write the code that implements the aspect. This could be the plumbing code required for the Circuit Breaker aspect as shown in listing 11.1. Once you’ve done this, you must tell the dynamic Interception library about the Abstractions it should apply the aspect to. Enough with the theory, let’s see an example.

11.1.1 示例:使用 Castle 动态代理进行拦截

11.1.1 Example: Interception with Castle Dynamic Proxy

凭借其重复代码,列表中的 Circuit Breaker 方面是动态拦截的一个很好的候选者。虽然你可以编写在运行时生成装饰器的代码,但这是一个相当复杂的操作,而且已经有很好的工具可用。我们将直接开始使用工具,而不是带您完成手动生成代码的繁琐过程。例如,让我们看看如何使用 Castle Dynamic Proxy 的拦截功能减少代码重复。

With its repetitive code, the Circuit Breaker aspect from listing is a good candidate for dynamic Interception. While you can write the code that generates Decorators at runtime, this is a rather involved operation, and besides, there are already excellent tools available. Instead of taking you through the tedious process of generating code by hand, we’ll start using a tool directly. As an example, let’s see how you can reduce code duplication with Castle Dynamic Proxy’s Interception capabilities.

实现断路器拦截器

Implementing a Circuit Breaker Interceptor

为 Castle 实现一个拦截器需要你实现它的Castle.DynamicProxy.IInterceptor接口,它由一个方法组成。以下清单显示了如何实现清单 11.1中的断路器。然而,与该列表不同的是,下面显示了整个类。

Implementing an Interceptor for Castle requires that you implement its Castle.DynamicProxy.IInterceptor interface, which consists of a single method. The following listing shows how to implement the Circuit Breaker from listing 11.1. Distinct from that listing, however, the following shows the entire class.

清单 11.2 使用动态代理实现断路器拦截器

Listing 11.2 Implementing the Circuit Breaker Interceptor with Dynamic Proxy

public class CircuitBreakerInterceptor
    : Castle.DynamicProxy.IInterceptor    ①  
{
    private readonly ICircuitBreaker breaker;

    public CircuitBreakerInterceptor(    ②  
        ICircuitBreaker breaker)
    {
        this.breaker = breaker;
    }

    public void Intercept(IInvocation invocation)    ③  
    {
        this.breaker.Guard();

        try
        {
            invocation.Proceed();    ④  

            this.breaker.Succeed();
        }
        catch (Exception ex)
        {
            this.breaker.Trip(ex);
            throw;
        }
    }
}

与清单 11.1的主要区别在于,您必须更通用,而不是将方法调用委托给特定方法,因为您可以将此代码应用于任何方法。IInvocation界面_传递给Intercept方法作为参数表示方法调用。例如,它可能代表对Insert(Product)方法的调用。Proceed方法_是此接口的关键成员之一,因为它使您能够让调用继续进行到堆栈上的下一个实现。

The main difference from listing 11.1 is that instead of delegating the method call to a specific method, you must be more general, because you apply this code to potentially any method. The IInvocation interface passed to the Intercept method as a parameter represents the method call. It might, for example, represent the call to the Insert(Product) method. The Proceed method is one of the key members of this interface, because it enables you to let the call proceed to the next implementation on the stack.

IInvocation界面_使您能够在让调用继续之前分配一个返回值。它还提供对有关方法调用的详细信息的访问。从调用参数中,你可以得到方法的名称和参数值的信息,以及当前方法调用的其他信息。实施拦截器是困难的部分。下一步很简单。

The IInvocation interface enables you to assign a return value before letting the call proceed. It also provides access to detailed information about the method call. From the invocation parameter, you can get information about the name and parameter values of the method, as well as other information about the current method call. Implementing the Interceptor is the hard part. The next step is easy.

使用纯 DI在组合根内应用拦截器

Applying the Interceptor inside the Composition Root using Pure DI

以下清单显示了如何将 合并CircuitBreakerInterceptor到您的Composition Root中。

The following listing shows how you can incorporate the CircuitBreakerInterceptor into your Composition Root.

清单 11.3 将拦截器合并到组合根中

Listing 11.3 Incorporating the Interceptor into the Composition Root

var generator =
    new Castle.DynamicProxy.ProxyGenerator();    ①  

var timeout = TimeSpan.FromMinutes(1);

var breaker = new CircuitBreaker(timeout);

var interceptor =
    new CircuitBreakerInterceptor(breaker);    ②  

var wcfRepository = new WcfProductRepository();    ③  

IProductRepository repository = generator    ④  
   .CreateInterfaceProxyWithTarget<IProductRepository>(  ④  
        wcfRepository,    ④  
        interceptor);    ④  

这个例子表明,虽然 Castle 控制着 Decorator 的构造和它的DependenciesIProductRepository的注入,你仍然可以使用Pure DI引导你的应用程序。在下一节中,我们将分析动态拦截并讨论其优缺点。

This example shows that, although Castle is in control of the construction of the IProductRepository Decorator and the injection of its Dependencies, you can still bootstrap your application using Pure DI. In the next section, we’ll analyze dynamic Interception and discuss its advantages and disadvantages.

11.1.2 动态拦截分析

11.1.2 Analysis of dynamic Interception

当我们将动态拦截的基于工具的 AOP 方法与上一章讨论的通过设计方法进行的 AOP 方法进行比较时,我们发现两者之间有许多相似之处:

When we compare the tool-based AOP approach of dynamic Interception to the AOP by design approach discussed in the previous chapter, we find a number of similarities between the two:

  • 当您针对抽象进行编程时,每一个都使您能够解决横切关注点
  • Each enables you to address Cross-Cutting Concerns when you program against Abstractions.
  • 与普通的旧装饰器一样,拦截器可以使用构造函数注入,这使它们对 DI 友好并且与它们正在装饰的代码分离。这些特性使您的业务代码和方面都可以轻松测试。
  • As with plain old Decorators, Interceptors can use Constructor Injection, which makes them DI-friendly and decoupled from the code they’re decorating. These characteristics allow both your business code and your aspects to be easily tested.
  • 方面可以集中在Composition Root中,这可以防止代码重复,如果您的 Visual Studio 解决方案包含多个应用程序,它允许方面在一个Composition Root中应用,但不能在另一个中应用。
  • Aspects can be centralized in the Composition Root, which prevents code duplication, and in case your Visual Studio solution contains multiple applications, it allows the aspects to be applied in one Composition Root, but not the other.

尽管有这些相似之处,但仍有一些差异使动态拦截不太理想。表 11.1总结了我们接下来要讨论的缺点。

Despite these similarities, there are some differences that make dynamic Interception less than ideal. Table 11.1 summarizes the downsides, which we’ll discuss next.

表 11.1动态拦截 的缺点
坏处概括
失去编译时支持.拦截代码往往比装饰器更复杂,这使得它更难阅读和维护。
方面与工具紧密耦合。这种耦合使得测试变得更加困难,并强制拦截器成为组合根的一部分,以防止其他程序集需要对动态拦截库的依赖。
不是普遍适用的。方面只能应用于虚拟或抽象方法的边界,例如作为接口定义一部分的方法。
不解决底层设计问题。与基于SOLID的设计相比,您仍然会得到一个系统,它的可维护性仅略高一点,而可维护性要差得多。

失去编译时支持

Loss of compile-time support

与普通的旧装饰器相比,动态拦截在任何时候使用拦截器时都涉及大量的运行时反射调用。例如,对于 Castle,IInvocation接口包含一个属性,该属性返回包含方法参数列表的实例Arguments数组。object在整数和布尔值类型的情况下,读取和更改这些值涉及强制转换和装箱。从性能的角度来看,这种持续不断的反思负担在很大程度上可以忽略不计。典型的 I/O 操作,例如数据库读写,成本要高出几个数量级。

Compared to plain old Decorators, dynamic Interception involves a fair deal of runtime reflection calls any time an Interceptor is used. With Castle, for instance, the IInvocation interface contains an Arguments property that returns an array of object instances that contains the list of method arguments. Reading and changing those values involves casting and boxing in case of value types like integer and boolean. From a performance perspective, this constant burden of reflection will be, for the most part, negligible. Your typical I/O operations, such as database reads and writes, cost orders of magnitude more.

然而,这种反射的使用确实使您编写的拦截器变得复杂。在处理方法参数列表和返回类型时,您必须编写正确的转换和类型检查,并可能更有效地传达转换错误。因此,拦截器往往比装饰器更复杂,这使得它更难阅读和维护。

This use of reflection, however, does complicate the Interceptors you write. When handling the list of method arguments and return types, you’ll have to write the proper casting and type checking, and possibly communicate casting errors more effectively. An Interceptor, therefore, tends to be more complicated than a Decorator, which makes it harder to read and maintain.

方面与工具紧密耦合

Aspects are strongly coupled to tooling

与普通的旧装饰器相比,您使用动态拦截编写的拦截器与您使用的拦截库强耦合。清单11.2的代码就是一个很好的例子。这个拦截器实现并使用了抽象CircuitBreakerInterceptorCastle.DynamicProxy.IInterceptorCastle.DynamicProxy.IInvocation

Compared to plain old Decorators, the Interceptors you write with dynamic Interception are strongly coupled to the Interception library you use. The CircuitBreakerInterceptor of listing 11.2 is a good example of this. This Interceptor implements Castle.DynamicProxy.IInterceptor and makes use of the Castle.DynamicProxy.IInvocation Abstraction.

尽管不如编译时编织那么普遍,正如您将在 11.2 节中看到的那样,这导致所有方面都耦合到 Castle Dynamic Proxy 库。这种耦合引入了对需要学习的外部库的额外依赖,这给项目带来了额外的成本和风险。我们将在 12.3.1 节中详细解释这一点。

Although less pervasive than compile-time weaving, as you’ll see in section 11.2, this leads to all aspects being coupled to a Castle Dynamic Proxy library. This coupling introduces an extra dependency on an external library that needs to be learned, which brings extra costs and risks to the project. We’ll explain this in detail in section 12.3.1 .

不普遍适用

Not universally applicable

因为动态拦截通过用动态生成的装饰器包装现有的抽象来工作,所以类的行为只能在抽象的方法边界处扩展。私有方法不能被拦截,因为它们不是接口的一部分。

Because dynamic Interception works by wrapping existing Abstractions with dynamically generated Decorators, the behavior of a class can only be extended at the Abstraction’s method boundaries. Private methods can’t be Intercepted because they’re not part of the interface.

在通过设计实践 AOP 时,此限制也适用。然而,通过设计 AOP,这通常不是什么问题,因为您以这样一种方式设计抽象,即只需要在抽象的边界应用方面。2另一方面,  当您应用动态拦截时,您通常会接受现状,因为如果您不这样做,您最终会按设计实践 AOP。

This limitation also holds true when practicing AOP by design. With AOP by design, however, this is typically less of a problem, because you design your Abstractions in such a way that there’s only a need to apply aspects at the boundaries of the Abstractions.2  When you apply dynamic Interception, on the other hand, you typically accept the status quo because, if you didn’t, you’d end up practicing AOP by design.

它不能解决底层设计问题

It doesn’t fix underlying design problems

IProductService在第 10 章中,我们广泛讨论了大界面存在的设计问题以及如何通过应用SOLID原则来修复它们。正如所讨论的,这些问题对系统的影响比任何关于跨领域关注点的问题都大.

In chapter 10, we extensively discussed the design problems that existed with the big IProductService interface and how they could be fixed by applying SOLID principles. As discussed, these problems have a bigger impact on the system beyond any issues regarding Cross-Cutting Concerns.

但是,当您接受应用程序当前设计的现状时,您可以使用动态拦截。您希望能够应用横切关注点而不必应用大型重构。这样做的缺点是你只能解决部分问题。您仍然会得到一个系统,该系统仅比现有设计更易于维护,并且比基于SOLID的设计更难维护。这是因为动态拦截仅考虑横切关注点的应用——而不是代码的其他部分.

You can use dynamic Interception, however, when you accept the status quo of the application’s current design. You want to be able to apply Cross-Cutting Concerns without having to apply large refactorings. The disadvantage of this is that you only solve part of the problem. You’ll still end up with a system that’s only marginally more maintainable than the existing design and considerably less maintainable than a more SOLID-based design This is because dynamic Interception only considers the application of Cross-Cutting Concerns — not other parts of your code.

应用动态拦截要求您针对接口进行编程并使用第 4 章中的 DI 模式。另一种不需要针对接口进行编程的 AOP 形式是编译时织入。乍一看这听起来很有吸引力,但正如我们接下来要讨论的那样,它是一种 DI 反模式。

Applying dynamic Interception requires you to program to interfaces and to use the DI patterns from chapter 4. Another form of AOP that doesn’t require programming to interfaces is compile-time weaving. This may sound attractive at first, but as we’ll discuss next, it’s a DI anti-pattern.

11.2 编译时织入

11.2 Compile-time weaving

当我们作为开发人员编写 C# 代码时,C# 编译器会将我们的代码转换为 Microsoft 中间语言(IL). IL 由 Common 读取语言运行时 (CLR) 即时 (JIT) 编译器并当场翻译成机器指令供 CPU 执行。3  您很可能熟悉此过程的基础知识。

When we as developers write C# code, the C# compiler transforms our code to Microsoft Intermediate Language (IL). IL is read by the Common Language Runtime (CLR) Just-In-Time (JIT) compiler and is translated on the spot to machine instructions for execution by the CPU.3  You’ll most likely be familiar with the basics of this process.

编译时编织是一种常见的 AOP 技术,它改变了这个编译过程。它使用特殊工具读取由我们的 (C#) 编译器生成的已编译程序集,对其进行修改,然后将其写回磁盘,从而有效地替换原始程序集。图 11.3显示了这个过程。

Compile-time weaving is a common AOP technique that alters this compilation process. It uses special tools to read a compiled assembly produced by our (C#) compiler, modifies it, and writes it back to disk, effectively replacing the original assembly. Figure 11.3 shows this process.

11-03.eps

图 11.3 编译时编织过程

Figure 11.3 Compile-time weaving process

在编译后过程中更改最初编译的程序集是为了将方面编织到原始源代码中,如图 11.4所示。

Altering an originally compiled assembly in a post-compilation process is done with the intention of weaving aspects into the original source code, as shown in figure 11.4.

11-04.eps

图 11.4 编译时编织,可视化

Figure 11.4 Compile-time weaving, visualized

但是,尽管一开始看起来很诱人,但当应用于易失性依赖项时,编译时编织的使用会带来一些问题,从可维护性的角度来看,这种技术存在问题。由于这些缺点,正如本节中所解释的那样,我们认为编译时织入是 DI 的对立面——它是一种 DI 反模式。

But, as alluring as it may seem at first, when applied to Volatile Dependencies, the use of compile-time weaving comes with issues that make this technique problematic from a maintainability perspective. Because of these downsides, as explained throughout this section, we consider compile-time weaving to be the opposite of DI — it’s a DI anti-pattern.

正如我们在简介中所述,我们发现讨论编译时织入很重要,即使它是一种 DI 反模式。编译时编织是一种众所周知的 AOP 形式,我们必须警告不要使用它。在我们讨论为什么它有问题之前,我们将从一个例子开始。

As we stated in the introduction, we found it important to discuss compile-time weaving even though it’s a DI anti-pattern. Compile-time weaving is such a well-known form of AOP that we have to warn against its use. Before we discuss why it’s problematic, we’ll begin with an example.

11.2.1 示例:使用编译时编织应用事务切面

11.2.1 Example: Applying a transaction aspect using compile-time weaving

属性与装饰器有一个共同特征:尽管它们可能添加或暗示对成员行为的修改,但它们保留签名和原始源代码不变。在 9.2.3 节中,您使用装饰器应用了安全方面。但是,编译时编织工具允许您通过将属性放在类、它们的成员甚至程序集上来声明方面。

Attributes share a trait with Decorators: although they may add or imply a modification of behavior of a member, they leave the signature and original source code unchanged. In section 9.2.3, you applied a security aspect using a Decorator. Compile-time weaving tools, however, let you declare aspects by placing attributes on classes, their members, and even assemblies.

使用这个概念来应用Cross-Cutting Concerns听起来很有吸引力。如果你可以用[Transaction]属性装饰一个方法或类,那不是很好吗,甚至是自定义[CircuitBreaker]属性并且,以这种方式,用一行声明性代码应用方面?以下清单显示了如何将自定义方面属性直接应用于.TransactionAttributeSqlProductRepository

It sounds attractive to use this concept to apply Cross-Cutting Concerns. Wouldn’t it be nice if you could decorate a method or class with a [Transaction] attribute, or even a custom [CircuitBreaker] attribute and, in this way, apply the aspect with a single line of declarative code? The following listing shows how a custom TransactionAttribute aspect attribute gets applied directly to the methods of SqlProductRepository.

清单 11.4[Transaction]aspect 属性应用于SqlProductRepository

Listing 11.4 Applying a [Transaction] aspect attribute to SqlProductRepository

public class SqlProductRepository : IProductRepository
{
    [Transaction]    ①  
    public void Insert(Product product) ...

    [Transaction]    ①  
    public void Update(Product product) ...

    [Transaction]    ①  
    public void Delete(Guid id) ...

    public IEnumerable<Product> GetAll() ...    ③  

    ...
}

虽然有很多编译时编织工具可供选择,但在本节中,我们将使用 PostSharp( https://www.postsharp.net/ ),这是一个商业工具。下一个清单显示了使用 PostSharp 的定义。TransactionAttribute

Although there are many compile-time weaving tools you can choose from, in this section, we’ll use PostSharp (https://www.postsharp.net/), which is a commercial tool. The next listing shows the definition of TransactionAttribute using PostSharp.

清单 11.5使用 PostSharp 实现方面TransactionAttribute

Listing 11.5 Implementing a TransactionAttribute aspect with PostSharp

[AttributeUsage(AttributeTargets.Method |    ①  
    AttributeTargets.Class |    ①  
    AttributeTargets.Assembly,    ①  
    AllowMultiple = false)]    ①  
[PostSharp.Serialization.PSerializable]    ①  
[PostSharp.Extensibility.MulticastAttributeUsage(  ①  
    MulticastTargets.Method,    ①  
    TargetMemberAttributes =    ①  
        MulticastAttributes.Instance |    ①  
        MulticastAttributes.Static)]    ①  
public class TransactionAttribute
    : PostSharp.Aspects.OnMethodBoundaryAspect    ③  
{

    public override void OnEntry(    ④  
        MethodExecutionArgs args)    ④  
    {    ④  
         args.MethodExecutionTag =    ④  
            new TransactionScope();    ④  
    }    ④  
    ④  
    public override void OnSuccess(    ④  
        MethodExecutionArgs args)    ④  
    {    ④  
        var scope = (TransactionScope)    ④  
            args.MethodExecutionTag;    ④  
        scope.Complete();    ④  
    }    ④  
    ④  
    public override void OnExit(    ④  
        MethodExecutionArgs args)    ④  
    {    ④  
        var scope = (TransactionScope)    ④  
            args.MethodExecutionTag;    ④  
        scope.Dispose();    ④  
    }    ④  
}

因为您想围绕任意一段代码包装事务,所以您需要重写 的三个方法— 即、和。在 期间,您创建一个新的 ,在 期间,您处理范围。保证被调用。PostSharp 会将其调用包装在一个块中。只有当包装操作成功时,您才会调用该方法。这就是为什么你在方法中实现它OnMethodBoundaryAspectOnEntryOnSuccessOnExitOnEntryTransactionScopeOnExitOnExitfinallyCompleteOnSuccess. 你利用MethodExecutionTag财产将创建TransactionScope的 from 方法转移到方法。

Because you want to wrap a transaction around some arbitrary piece of code, you need to override three of the methods of OnMethodBoundaryAspect — namely, OnEntry, OnSuccess, and OnExit. During OnEntry, you create a new TransactionScope, and during OnExit, you dispose of the scope. OnExit is guaranteed to be called. PostSharp will wrap its call in a finally block. Only when the wrapped operation succeeds will you want to invoke the Complete method. That’s why you implement this in the OnSuccess method. You make use of the MethodExecutionTag property to transfer the created TransactionScope from method to method.

单独查看清单 11.4时,您可能会发现这些属性很有吸引力,但如果将清单 11.5中的代码与装饰器(清单 10.15)中的相同方面进行比较,就会发现有很多样板文件。您需要重写多个方法,应用各种属性,并将状态从一个方法传递到另一个方法。

While looking at listing 11.4 in isolation, you might find these attributes attractive, but if you compare the code in listing 11.5 to the same aspect in a Decorator (listing 10.15), there’s quite a lot of boilerplate. You need to override multiple methods, apply all kinds of attributes, and pass state from method to method.

如果这会增加可维护性,这可能是一个很小的代价,但是编译时编织还有其他更多限制性问题,这使得它不适合作为将易失性依赖项作为横切关注点应用的方法。

This would perhaps be a small price to pay if this would increase maintainability, but there are other, more limiting issues with compile-time weaving that make it unsuitable as a method to apply Volatile Dependencies as Cross-Cutting Concerns.

11.2.2 编译时织入分析

11.2.2 Analysis of compile-time weaving

关于 DI,编译时织入有两个特定的缺点。在本节中,我们将讨论这些限制。虽然还有其他缺点编译时编织,表 11.2中描述的两个捕获了核心问题,使其成为 DI 不受欢迎的方法。

In relationship to DI, compile-time weaving comes with two specific disadvantages. In this section, we’ll discuss these limitations. While there are other downsides to compile-time weaving, the two described in table 11.2 capture the core issue that makes it an undesirable method for DI.

表 11.2 从 DI 角度看编译时织入的缺点
坏处概括
DI 不友好没有好的方法可以将易失性依赖项注入编译时编织方面。备选方案会导致时间耦合俘虏依赖和相互依赖测试。
编译时耦合方面是在编译时编织的,如果没有应用方面就不可能调用代码。这使测试复杂化并降低了灵活性。

编译时编织方面对 DI 不友好

Compile-time weaving aspects are DI-unfriendly

在应用Cross-Cutting Concerns时,您会发现自己经常使用Volatile Dependencies。正如您在第 1 章中了解到的,易失性依赖项是 DI 的焦点。对于Volatile Dependencies,您的默认选择应该是使用Constructor Injection,因为它静态定义了所需依赖项的列表。

When it comes to applying Cross-Cutting Concerns, you’ll find yourself regularly working with Volatile Dependencies. As you learned in chapter 1, Volatile Dependencies are the focal point of DI. With Volatile Dependencies, your default choice should be to use Constructor Injection, because it statically defines the list of required Dependencies.

不幸的是,不可能在编译时编织方面使用构造函数注入。看看下一个清单,我们尝试在断路器方面使用构造函数注入。

Unfortunately, it isn’t possible to use Constructor Injection with compile-time weaving aspects. Take a look at the next listing, where we try to use Constructor Injection with a Circuit Breaker aspect.

坏.tif

清单 11.6使用构造函数注入依赖项注入方面

Listing 11.6 Injecting a Dependency into an aspect using Constructor Injection

[PostSharp.Serialization.PSerializable]    ①  
public class CircuitBreakerAttribute
    : OnMethodBoundaryAspect
{
    private readonly ICircuitBreaker breaker;

    public CircuitBreakerAttribute(    ②  
        ICircuitBreaker breaker)
    {
        this.breaker = breaker;
    }

    public override void OnEntry(
        MethodExecutionArgs args)
    {
        this.breaker.Guard();    ③  
    }

    ...
}

将构造函数注入应用于此方面类的尝试惨遭失败。请记住,您正在定义一个代表单独代码的属性,它将是编织到您将在编译时使用的方法中。在 .NET 中,属性在其构造函数中只能具有原始类型,例如字符串和整数。

This attempt to apply Constructor Injection to this aspect class fails miserably. Remember, you’re defining an attribute that represents separate code, which will be woven into the methods you’ll be working with at compile time. In .NET, attributes can only have primitive types, such as strings and integers, in their constructor.

即使属性可能具有更复杂的Dependencies,您也无法为该方面的实例提供一个实例,因为该方面是在与您构造实例的位置完全不同的时间和地点构造的。属性的实例,如,是由 .NET 运行时创建的,您无法影响它们的创建。您无法将Dependency作为Composition Root的一部分注入到属性的构造函数中:ICircuitBreakerICircuitBreakerCircuitBreakerAttribute

Even if attributes could have more complex Dependencies, there’d be no way for you to supply an instance of this aspect with an ICircuitBreaker instance, because the aspect is constructed at a completely different time and location from where you’d construct ICircuitBreaker instances. Instances of attributes, like the CircuitBreakerAttribute, are created by the .NET runtime, and there’s no way for you to influence their creation. You have no means of injecting the Dependency into the attribute’s constructor as part of, for instance, the Composition Root:

11-05_hedgehog.eps

然而,这个问题并不局限于使用属性。即使 AOP 框架使用属性以外的机制,它的后编译器也会在编译时将方面代码编织到您的正常代码中,并使其成为程序集代码的一部分。另一方面,您的对象图是在运行时作为Composition Root的一部分构建的。这两个模型不能很好地混合。编译时编织不可能进行构造函数注入。

This issue, however, isn’t limited to working with attributes. Even if the AOP framework uses a mechanism other than attributes, its post-compiler weaves the aspect code into your normal code at compile time and makes it part of the assembly’s code. Your object graphs, on the other hand, are constructed at runtime as part of the Composition Root. These two models don’t mix well. Constructor Injection isn’t possible with compile-time weaving.

环境语境服务定位器有两个解决此问题的方法。然而,这两种解决方法都是黑客攻击,它们都有相当大的缺点。为了争论,让我们看看如何使用Ambient Context来解决这个问题。以下清单显示了Breaker断路器方面的公共静态属性的定义。

Ambient Context and Service Locator are two workarounds for this issue. Both workarounds are, however, hacks with considerable downsides of their own. For the sake of argument, let’s take a look at how to work around the problem using an Ambient Context. The following listing shows the definition of a public static Breaker property in the Circuit Breaker aspect.

坏.tif

清单 11.7在使用环境上下文的方面内 使用依赖关系

Listing 11.7 Using a Dependency inside an aspect using an Ambient Context

public class CircuitBreakerAttribute
    : OnMethodBoundaryAspect
{
    public static ICircuitBreaker Breaker { get; set; }  ①  

    public override void OnEntry(
        MethodExecutionArgs args)
    {
        Breaker.Guard();
    }

    ...
}

正如您在 5.3.2 节中了解到的,除其他事项外,环境上下文会导致时间耦合。这意味着如果您忘记设置Breaker属性,应用程序将失败并显示,因为依赖项不是可选的。NullReferenceException

As you learned in section 5.3.2, among other things, the Ambient Context causes Temporal Coupling. This means that if you forget to set the Breaker property, the application fails with a NullReferenceException, because the Dependency isn’t optional.

此外,由于您唯一的选择是在应用程序启动期间设置该属性一次,因此需要将其定义为static. 但这可能会导致其自身的问题:如第 8.4.1 节所述,这可能会导致ICircuitBreaker成为俘虏依赖项。

Further, because your only option is to set the property once during application startup, it needs to be defined as static. But this might lead to problems of its own: this could cause the ICircuitBreaker to become a Captive Dependency, as explained in section 8.4.1.

这样的static属性会导致相互依赖测试,因为它的值在执行下一个测试用例时仍保留在内存中。因此有必要在每次测试后执行 Fixture Teardown。4  这是我们必须始终记住要做的事情——很容易忘记。出于这个原因,使用环境上下文访问易失性依赖项的编译时编织方面不容易测试。

Such a static property causes Interdependent Tests because its value remains in memory when the next test case is executed. It’s therefore necessary to perform Fixture Teardown after each and every test.4  This is something that we must always remember to do — it’s easy to forget. For this reason, compile-time weaving aspects that use an Ambient Context to access Volatile Dependencies aren’t easy to test.

另一个解决方法是Service Locator,但与Ambient Context相比,它只会让事情变得更糟。Service Locator在 Interdependent Tests 和Temporal Coupling中表现出同样的问题。最重要的是,它对一组未绑定的Volatile Dependencies的访问使得它的Dependencies是什么并不明显,并且它将Service Locator作为冗余Dependency拖拽。因为Service Locator是更糟糕的选择,我们为您省去了一个例子,直接跳到编译时织入的第二个缺点——编译时耦合。

The other workaround is Service Locator, but compared to Ambient Context, it’d only make things worse. Service Locator exhibits the same problems with Interdependent Tests and Temporal Coupling. On top of that, its access to an unbound set of Volatile Dependencies makes it non-obvious as to what its Dependencies are, and it drags along the Service Locator as a redundant Dependency. Because Service Locator is the worse choice, we spare you an example and jump directly into the second disadvantage of compile-time weaving — coupling at compile time.

编译时编织导致编译时耦合

Compile-time weaving causes coupling at compile time

尽管编译时织入将代码与切面分离,但它仍然会导致编译后的代码与织入切面紧密耦合。这是一个问题,因为Cross-Cutting Concerns通常依赖于外部系统。当您编写单元测试时,这个问题变得很明显,因为单元测试必须能够独立运行。您想要测试一个类的逻辑本身,而不与其Volatile Dependencies相互依赖。您不希望单元测试跨越进程和网络边界,因为与数据库、文件系统或其他外部系统的通信会影响测试的可靠性和性能。换句话说,编译时编织方面影响可测试性.

Although compile-time weaving decouples your source code from your aspects, it still causes your compiled code to be tightly coupled with the woven aspects. This is a problem, because Cross-Cutting Concerns often depend on an external system. This problem becomes obvious when you write unit tests, because a unit test must be able to run in isolation. You want to test a class’s logic itself without interdependency with its Volatile Dependencies. You don’t want your unit test crossing process and network boundaries, because communication with a database, filesystem, or other external system will influence the reliability and performance of your tests. In other words, compile-time woven aspects impact Testability.

但即使使用广泛定义的集成测试,编译时织入仍会导致问题。在集成测试中,您测试系统的一部分与其他部分的集成。这降低了隔离级别,但使您能够了解各个组件在与其他组件集成时如何工作。例如,如果您正在测试SqlProductRepository,那么对其进行单元测试就没有意义,因为这个 Repository 所做的只是查询数据库。因此,您想要测试该组件与数据库的交互。

But even with broadly defined integration tests, compile-time weaving will still cause problems. In an integration test, you test a part of the system in integration with other parts. This lowers the level of isolation, but enables you to find out how individual components work when integrated with others. If you were testing SqlProductRepository, for instance, it wouldn’t make sense to unit test it, because all this Repository does is query the database. You therefore want to test this component’s interaction with the database.

但即使在那种情况下,您通常也不希望在测试期间应用所有方面。一个[CheckAuthorization]方面的使用,例如,可能会强制这样的测试通过某种登录过程来验证组件是否可以成功存储和检索产品。重要的是要查看这样的授权方面是否按预期工作。不幸的是,必须将此作为每个集成测试的测试设置的一部分运行,这会使这些测试更难维护,而且可能会慢很多。

But even in that case, you typically wouldn’t want to have all aspects applied during testing. The use of a [CheckAuthorization] aspect, for instance, might force such a test to go through some sort of login process to verify whether the component can successfully store and retrieve products. It’s important to see whether such an authorization aspect works as expected. Having to run this as part of your test setup for every integration test, unfortunately, makes these tests harder to maintain and, possibly, a lot slower.

如果您还启用了缓存,则会出现一个更有趣的问题。在这种情况下,您可以编写一个自动化测试来查询数据库,但永远不要这样做,因为测试代码会命中缓存。出于这个原因,您希望完全控制将哪些方面应用于哪个测试以及何时应用,以防这些方面与Volatile Dependencies 相关。编译时编织使这变得非常复杂。

A funnier problem manifests itself if you also have caching enabled. In such a case, you could write an automated test with the intent to query the database, but never do so because the test code hits the cache. For this reason, you want to have full control over which aspects are applied to which test and when, in case those aspects are related to Volatile Dependencies. Compile-time weaving complicates this tremendously.

编译时编织不适合用于易失性依赖项

Compile-time weaving is unsuitable for use on Volatile Dependencies

DI 的目的是通过将Seams引入您的应用程序来管理易失性依赖关系。这使您能够将对象图的组合集中在Composition Root中。

The aim of DI is to manage Volatile Dependencies by introducing Seams into your application. This enables you to centralize the composition of your object graphs inside the Composition Root.

这与您在应用编译时织入时获得的结果完全相反:它导致易失性依赖项在编译时耦合到您的代码。这使得无法使用正确的 DI 技术并在应用程序的组合根中安全地组合完整的对象图。正是出于这个原因,我们说编译时织入与 DI 相反——在Volatile Dependencies上使用编译时织入是一种反模式。

This is the complete opposite of what you achieve when applying compile-time weaving: it causes Volatile Dependencies to be coupled to your code at compile time. This makes it impossible to use proper DI techniques and to safely compose complete object graphs in the application’s Composition Root. It’s for this reason that we say that compile-time weaving is the opposite of DI — using compile-time weaving on Volatile Dependencies is an anti-pattern.

赞成应用SOLID原则,如果不可能则回退到动态拦截。关于这一点,我们现在可以在第 3 部分中将Pure DI抛在脑后,继续阅读第 4 部分中的DI 容器。在那里,您将了解DI 容器如何解决您可能面临的一些挑战。

Favor applying SOLID principles, falling back to dynamic Interception if that isn’t possible. On that note, we can now leave Pure DI behind in part 3 and move on to read about DI Containers in part 4. There, you’ll learn how DI Containers can fix some of the challenges you might face.

概括

Summary

  • 动态拦截是一种面向方面的编程(AOP) 技术,可自动生成要在运行时发出的装饰器。方面被编写为拦截器,它们被注入到运行时生成的装饰器中。
  • Dynamic Interception is an Aspect-Oriented Programming (AOP) technique that automates the generation of Decorators to be emitted at runtime. Aspects are written as Interceptors, which are injected into a runtime-generated Decorator.
  • 动态拦截具有以下缺点:
    • 失去编译时支持。
    • 方面与工具紧密耦合。
    • 不是普遍适用的。
    • 不解决底层设计问题。
  • Dynamic Interception exhibits the following disadvantages:
    • Loss of compile-time support.
    • Aspects are strongly coupled to the tooling.
    • Not universally applicable.
    • Doesn’t fix underlying design problems.
  • 为了防止或延迟像我们在第 10 章中建议的那样进行设计更改,动态拦截可能是一个很好的临时解决方案,直到开始进行这些类型的改进。
  • To prevent or delay making design changes like the ones we suggested in chapter 10, dynamic Interception might be a good temporary solution until its time to start making these kinds of improvements.
  • 编译时编织是一种改变编译过程的 AOP 技术。它使用特殊工具通过 IL 操作更改已编译的程序集。这不是将 AOP 应用于Volatile Dependencies的理想方法。
  • Compile-time weaving is an AOP technique that alters the compilation process. It uses special tools to alter a compiled assembly using IL manipulation. It isn’t a desirable method of applying AOP to Volatile Dependencies.
  • 关于 DI,编译时织入存在以下问题:
    • 编译时编织方面对 DI 不友好。
    • 编译时织入导致编译时紧耦合。
  • In relation to DI, compile-time weaving exhibits the following problems:
    • Compile-time weaving aspects are DI-unfriendly.
    • Compile-time weaving causes tight coupling at compile time.
  • 赞成应用SOLID原则,如果不可能则回退到动态拦截。
  • Favor applying SOLID principles, falling back to dynamic Interception if that isn’t possible.

第 4 部分

DI 容器

Part 4

DI Containers

本书的前几部分介绍了共同定义 DI 的各种原则和模式。正如第 3 章所解释的那样,DI 容器是一个可选工具,您可以使用它来实现许多通用基础设施,如果您使用Pure DI ,您将不得不实现这些基础设施。

The previous parts of the book have been about the various principles and patterns that together define DI. As chapter 3 explained, a DI Container is an optional tool that you can use to implement a lot of the general-purpose infrastructure that you would otherwise have to implement if you were using Pure DI.

在整本书中,我们一直在讨论容器不可知论,这意味着我们只教你Pure DI。不要将此解释为Pure DI本身的推荐;相反,我们希望您看到最纯粹形式的 DI,不受任何特定容器 API 的污染。

Throughout the book, we’ve kept the discussion container agnostic, which means we’ve only taught you Pure DI. Don’t interpret this as a recommendation of Pure DI per se; rather, we want you to see DI in its purest form, untainted by any particular container’s API.

许多出色的DI 容器可用于 .NET 平台。在第 12 章中,我们将讨论何时应使用其中一种容器以及何时应坚持使用Pure DI。第 4 部分的其余章节涵盖了三个免费和开源的DI 容器的选择。在每一章中,我们都详细介绍了特定容器的 API,因为它与第 3 部分中涵盖的维度相关,以及传统上让初学者感到悲伤的各种其他问题。涵盖的容器包括 Autofac(第 13 章)、Simple Injector(第 14 章)和 Microsoft.Extensions.DependencyInjection(第 15 章)。

Many excellent DI Containers are available for the .NET platform. In chapter 12, we’ll discuss when you should use one of these containers and when you should stick with Pure DI. The remaining chapters in part 4 cover a selection of three free and open source DI Containers. In each chapter, we provide detailed coverage of that particular container’s API as it relates to the dimensions covered in part 3, as well as various other issues that traditionally cause beginners grief. The containers covered are Autofac (chapter 13), Simple Injector (chapter 14), and Microsoft.Extensions.DependencyInjection (chapter 15).

考虑到无限的空间和时间,我们想包括所有容器,但遗憾的是,这是不可能的。除了第一版中涵盖的一个容器外,我们排除了所有容器。排除在外的包括 Castle Windsor、StructureMap、String.NET、Unity 和 MEF。有关这些的更多信息,请获取第一版的副本(此版本免费提供)。此外,我们考虑了但未包括 Ninject,它是更流行的DI 容器之一。在撰写本文时,没有可用的 .NET Core 兼容版本,这是纳入标准。

Given unlimited space and time, we wanted to include all containers, but alas, that wasn’t possible. We excluded all but one of the containers covered in the first edition. Those excluded include Castle Windsor, StructureMap, String.NET, Unity, and MEF. For more information on those, grab your copy of the first edition (you get it free with this edition). Also, we considered, but didn’t include, Ninject, which is one of the more popular DI Containers. At the time of writing, there is no .NET Core–compatible version available, which was a criterion for inclusion.

描述的所有容器都是具有快速发布周期的开源项目。在我们讨论本部分的容器之前,第 12 章会更详细地介绍关于什么是容器,它可以帮助您做什么,以及如何决定何时使用DI 容器或坚持使用Pure DI

All the containers described are open source projects with fast release cycles. Before we discuss the containers in this part, chapter 12 goes into more detail about what a container is, what it helps you with, and how to decide when to use a DI Container or stick with using Pure DI.

由于它的市场份额,我们不能简单地将 Autofac 排除在外,即使我们在第一版中介绍了它。Autofac 是最流行的 .NET DI 容器。第 13 章专门介绍它。尽管我们包含了 Microsoft.Extensions.DependencyInjection (MS.DI),但我们对其持怀疑态度,因为它的功能有限。然而,我们觉得有必要介绍一下,因为许多开发人员倾向于先使用内置工具,然后再切换到第三方工具。第 15 章将解释 MS.DI 能做什么和不能做什么。

Because of its market share, we simply couldn’t exclude Autofac, even though we covered it in the first edition. Autofac is the most popular DI Container for .NET. Chapter 13 is dedicated to it. And although we included Microsoft.Extensions.DependencyInjection (MS.DI), we’re skeptical of it, because it’s limited in functionality. However, we felt obliged to cover it, because many developers are inclined to use the built-in tooling first before switching to third-party tooling. Chapter 15 will explain what MS.DI can and can’t do.

每一章都遵循一个通用模板。当您第三次阅读同一个句子时,这可能会给您带来某种似曾相识的感觉。我们认为这是一个优势,因为如果您想比较特定功能如何跨容器解决,它应该可以让您轻松快速地找到不同章节中的相似部分。

Each chapter follows a common template. This may give you a certain sense of déjà vu as you read the same sentence for the third time. We consider it an advantage, because it should make it easy for you to quickly find similar sections across different chapters if you want to compare how a specific feature is addressed across containers.

这些章节旨在激发灵感。如果您还没有挑选出最喜欢的DI 容器,您可以通读所有三章来比较它们,但您也可以只阅读您特别感兴趣的那一章。第 4 部分中提供的信息在撰写本文时是准确的,但请始终确保也查阅更多最新资源。

These chapters are meant as inspiration. If you have yet to pick a favorite DI Container, you can read through all three chapters to compare them, but you can also just read the one that particularly interests you. The information presented in part 4 was accurate at the time of writing, but always be sure to consult more up-to-date sources as well.

12

DI容器介绍

12

DI Container introduction

在这一章当中

In this chapter

  • 使用配置文件启用后期绑定
  • Using configuration files to enable late binding
  • 使用配置即代码在DI 容器中显式注册组件
  • Explicitly registering components in a DI Container with Configuration as Code
  • 在具有自动注册的DI 容器中应用约定优于配置
  • Applying Convention over Configuration in a DI Container with Auto-Registration
  • 选择应用纯 DI还是使用DI 容器
  • Choosing between applying Pure DI or using a DI Container

当我(马克)还是个孩子的时候,我妈妈和我偶尔会做冰淇淋。这种情况并不经常发生,因为它需要工作,而且很难做到正确。真正的冰淇淋是以英式奶油为基础的这是一种由糖、蛋黄、牛奶或奶油制成的淡奶油冻。如果加热太多,这种混合物会凝结。即使您设法避免这种情况,下一阶段也会出现更多问题。单独放在冰箱里,奶油混合物会结晶,所以你必须定期搅拌它,直到它变得太硬以至于不再可能。只有这样,您才能享用美味的自制冰淇淋。虽然这是一个缓慢且劳动密集型的过程,但如果您愿意——并且您有必要的原料和设备——您可以使用这种技术来制作冰淇淋。

When I (Mark) was a kid, my mother and I would occasionally make ice cream. This didn’t happen too often because it required work, and it was hard to get right. Real ice cream is based on a crème anglaise, which is a light custard made from sugar, egg yolks, and milk or cream. If heated too much, this mixture curdles. Even if you manage to avoid this, the next phase presents more problems. Left alone in the freezer, the cream mixture crystallizes, so you have to stir it at regular intervals until it becomes so stiff that this is no longer possible. Only then will you have a good, homemade ice cream. Although this is a slow and labor-intensive process, if you want to — and you have the necessary ingredients and equipment — you can use this technique to make ice cream.

今天,大约 35 年后,我岳母制作冰淇淋的频率是我和我母亲在更年轻时无法比拟的——不是因为她喜欢制作冰淇淋,而是因为她使用技术来帮助她。技术仍然相同,但她没有定期从冰箱中取出冰淇淋并搅拌,而是使用电动冰淇淋机为她完成这项工作(见图 12.1)。

Today, some 35 years later, my mother-in-law makes ice cream with a frequency unmatched by myself and my mother at much younger ages — not because she loves making ice cream, but because she uses technology to help her. The technique is still the same, but instead of regularly taking out the ice cream from the freezer and stirring it, she uses an electric ice cream maker to do the work for her (see figure 12.1).

12-01.tif

图 12.1 一家意大利冰淇淋制造商。与制作冰淇淋一样,有了更好的技术,您可以更轻松、更快速地完成编程任务。

Figure 12.1 An Italian ice cream maker. As with making ice cream, with better technology, you can accomplish programming tasks more easily and quickly.

DI 首先是一种技术,但您可以使用技术使事情变得更容易。在第 3 部分中,我们将 DI 描述为一种技术。在第 4 部分中,我们将了解可用于支持 DI 技术的技术。我们称这种技术为 DI 容器

DI is first and foremost a technique, but you can use technology to make things easier. In part 3, we described DI as a technique. Here, in part 4, we take a look at the technology that can be used to support the DI technique. We call this technology DI Containers.

在本章中,我们将把DI 容器视为一个概念——它们如何融入 DI 的整体主题——以及有关它们的使用的一些模式和实践。我们还将在此过程中查看一些示例。

In this chapter, we’ll look at DI Containers as a concept — how they fit into the overall topic of DI — as well as some patterns and practices concerning their usage. We’ll also look at some examples along the way.

本章从对DI 容器的一般介绍开始,包括对称为自动装配的概念的描述,然后是关于各种配置选项的部分。您可以单独阅读这些配置选项中的每一个,但我们认为在阅读自动注册之前至少阅读配置即代码是有益的.

This chapter begins with a general introduction to DI Containers, including a description of a concept called Auto-Wiring, followed by a section on various configuration options. You can read about each of these configuration options in isolation, but we think it’d be beneficial to at least read about Configuration as Code before you read about Auto-Registration.

最后一段不一样。它重点介绍了使用DI 容器的优点和缺点,并帮助您确定使用DI 容器是否对您和您的应用程序有益。我们认为这是每个人都应该阅读的重要部分,无论他们是否有使用 DI 和DI 容器的经验。本节可以单独阅读,但最好先阅读配置即代码自动注册部分。

The last section is different. It focuses on the advantages and disadvantages of using DI Containers and helps you decide whether the use of a DI Container is beneficial to you and your applications. We think this an important part that everyone should read, regardless of their experience with DI and DI Containers. This section can be read in isolation, although it would be beneficial to read the sections on Configuration as Code and Auto-Registration first.

本章的目的是让你更好地理解什么是DI 容器,以及它如何适应本书中的其他模式和原则。从某种意义上说,您可以将本章视为本书第 4 部分的介绍。在这里,我们将笼统地讨论DI 容器,而在接下来的章节中,我们将讨论特定的容器及其 ​​API。

The purpose of this chapter is to give you a good understanding of what a DI Container is and how it fits in with the rest of the patterns and principles in this book. In a sense, you can view this chapter as an introduction to part 4 of the book. Here, we’ll talk about DI Containers in general, whereas in the following chapters, we’ll talk about specific containers and their APIs.

12.1 引入DI 容器

12.1 Introducing DI Containers

DI 容器是一个软件库,可以自动执行对象组合生命周期管理拦截中涉及的许多任务。尽管可以使用Pure DI编写所有必需的基础结构代码,但它不会为应用程序增加太多价值。另一方面,组合对象的任务具有一般性,可以一劳永逸地解决;这就是所谓的通用子域. 1  鉴于此,使用通用库是有意义的。它与实现日志记录或数据访问没有太大区别;记录应用程序数据是一种可以通过通用日志记录库解决的问题。组合对象图也是如此。

A DI Container is a software library that can automate many of the tasks involved in Object Composition, Lifetime Management, and Interception. Although it’s possible to write all the required infrastructure code with Pure DI, it doesn’t add much value to an application. On the other hand, the task of composing objects is of a general nature and can be resolved once and for all; this is what’s known as a Generic Subdomain.1  Given this, using a general-purpose library can make sense. It’s not much different than implementing logging or data access; logging application data is the kind of problem that can be addressed by a general-purpose logging library. The same is true for composing object graphs.

在本节中,我们将讨论DI 容器如何组成对象图。我们还将向您展示一些示例,让您大致了解使用容器和实现的情况。

In this section, we’ll discuss how DI Containers compose object graphs. We’ll also show you some examples to give you a general sense of what using a container and an implementation might look like.

12.1.1 探索容器的 Resolve API

12.1.1 Exploring containers’ Resolve API

DI 容器是一个软件库,就像任何其他软件库一样。它公开了一个 API,您可以使用它来组合对象,并且组合对象图是一个单一的方法调用。DI 容器还要求您在组合对象之前配置它们。我们将在 12.2 节中重新讨论它。

A DI Container is a software library like any other software library. It exposes an API that you can use to compose objects, and composing an object graph is a single method call. DI Containers also require you to configure them prior to composing objects. We’ll revisit that in section 12.2.

在这里,我们将向您展示一些示例,说明DI 容器如何解析对象图。作为本节中的示例,我们将同时使用 Autofac 和 Simple Injector到 ASP.NET Core MVC 应用程序。有关如何编写 ASP.NET Core MVC 应用程序的更多详细信息,请参阅第 7.3 节。

Here, we’ll show you some examples of how DI Containers can resolve object graphs. As examples in this section, we’ll use both Autofac and Simple Injector applied to an ASP.NET Core MVC application. Refer to section 7.3 for more detailed information about how to compose ASP.NET Core MVC applications.

您可以使用DI 容器来解析控制器实例。可以使用以下章节中介绍的所有三个DI 容器来实现此功能,但我们将在此处仅展示几个示例。

You can use a DI Container to resolve controller instances. This functionality can be implemented with all three DI Containers covered in the following chapters, but we’ll show only a couple of examples here.

使用各种DI 容器解析控制器

Resolving controllers with various DI Containers

Autofac 是一个DI 容器,具有相当符合模式的 API。假设您已经有一个 Autofac 容器实例,您可以通过提供请求的类型来解析控制器:

Autofac is a DI Container with a fairly pattern-conforming API. Assuming you already have an Autofac container instance, you can resolve a controller by supplying the requested type:

var controller = (HomeController)container.Resolve(typeof(HomeController));

你将传递typeof(HomeController)Resolve方法并取回所请求类型的实例,其中完全填充了所有适当的Dependencies。该Resolve方法是弱类型的并返回一个实例System.Object;这意味着您需要将其转换为更具体的内容,如示例所示。

You’ll pass typeof(HomeController) to the Resolve method and get back an instance of the requested type, fully populated with all the appropriate Dependencies. The Resolve method is weakly typed and returns an instance of System.Object; this means you’ll need to cast it to something more specific, as the example shows.

许多DI 容器具有与 Autofac 类似的 API。Simple Injector 的相应代码看起来与 Autofac 的代码几乎相同,即使实例是使用SimpleInjector.Container该类解析的。使用 Simple Injector,之前的代码将如下所示:

Many of the DI Containers have APIs that are similar to Autofac’s. The corresponding code for Simple Injector looks nearly identical to Autofac’s, even though instances are resolved using the SimpleInjector.Container class. With Simple Injector, the previous code would look like this:

controller = (HomeController)container.GetInstance(typeof(HomeController));

唯一真正的区别是该Resolve方法被调用GetInstance。您可以从这些示例中提取DI 容器的一般形状。

The only real difference is that the Resolve method is called GetInstance. You can extract a general shape of a DI Container from these examples.

使用DI 容器解析对象图

Resolving object graphs with DI Containers

DI 容器是解析和管理对象图的引擎。尽管DI 容器不仅仅是解析对象,但这是任何容器 API 的核心部分。前面的例子表明容器有一个用于此目的的弱类型方法。随着名称和签名的变化,该方法如下所示:

A DI Container is an engine that resolves and manages object graphs. Although there’s more to a DI Container than resolving objects, this is a central part of any container’s API. The previous examples show that containers have a weakly typed method for that purpose. With variations in names and signatures, that method looks like this:

object Resolve(Type serviceType);

如前面的示例所示,由于返回的实例类型为System.Object,因此您通常需要在使用之前将返回值转换为预期的类型。许多DI 容器还为您知道在编译时请求哪种类型的情况提供通用版本。它们通常看起来像这样:

As the previous examples demonstrate, because the returned instance is typed as System.Object, you often need to cast the return value to the expected type before using it. Many DI Containers also offer a generic version for those cases where you know which type to request at compile time. They often look like this:

T Resolve<T>();

这样的重载不是提供Type方法参数,而是采用类型参数( T) 表示请求的类型。该方法返回 的一个实例T。如果无法解析请求的类型,大多数容器都会抛出异常。

Instead of supplying a Type method argument, such an overload takes a type parameter (T) that indicates the requested type. The method returns an instance of T. Most containers throw an exception if they can’t resolve the requested type.

如果我们Resolve孤立地查看该方法,它看起来就像魔术一样。从编译器的角度来看,可以要求它解析任意类型的实例。容器如何知道如何组合请求的类型,包括所有Dependencies?它没有;你必须先告诉它。您使用将抽象映射到具体类型的配置来执行此操作。我们将在 12.2 节回到这个主题。

If we view the Resolve method in isolation, it almost looks like magic. From the compiler’s perspective, it’s possible to ask it to resolve instances of arbitrary types. How does the container know how to compose the requested type, including all Dependencies? It doesn’t; you’ll have to tell it first. You do so using a configuration that maps Abstractions to concrete types. We’ll return to this topic in section 12.2.

如果容器没有足够的配置来完全组成请求的类型,它通常会抛出一个描述性异常。例如,考虑HomeController我们在清单 3.4 中首先讨论的以下内容。您可能还记得,它包含类型的依赖IProductService项:

If a container has insufficient configuration to fully compose a requested type, it’ll normally throw a descriptive exception. As an example, consider the following HomeController we first discussed in listing 3.4. As you might remember, it contains a Dependency of type IProductService:

public class HomeController : Controller
{
    private readonly IProductService productService;

    public HomeController(IProductService productService)
    {
        this.productService = productService;
    }

    ...
}

在配置不完整的情况下,Simple Injector 具有如下示例性异常消息:

With an incomplete configuration, Simple Injector has exemplary exception messages like this one:

HomeController 类型的构造函数包含名称为“productService”且类型为 IProductService 的参数,该参数未注册。请确保 IProductService 已注册或更改 HomeController 的构造函数。

The constructor of type HomeController contains the parameter with name 'productService' and type IProductService, which isn’t registered. Please ensure IProductService is registered or change the constructor of HomeController.

在前面的示例中,您可以看到 Simple Injector 无法 resolve HomeController,因为它包含 type 的构造函数参数,但是 Simple Injector 没有被告知在请求IProductService时返回哪个实现。IProductService如果容器配置正确,它甚至可以从请求的类型中解析复杂的对象图。如果配置中缺少某些内容,容器可以提供有关缺少内容的详细信息。在下一节中,我们将仔细研究这是如何完成的。

In the previous example, you can see that Simple Injector can’t resolve HomeController, because it contains a constructor argument of type IProductService, but Simple Injector wasn’t told which implementation to return when IProductService was requested. If the container is correctly configured, it can resolve even complex object graphs from the requested type. If something is missing from the configuration, the container can provide detailed information about what’s missing. In the next section, we’ll take a closer look at how this is done.

12.1.2 自动接线

12.1.2 Auto-Wiring

DI 容器在编译到所有类中的静态信息上茁壮成长。使用反射,他们可以分析请求的类并找出需要哪些依赖项。

DI Containers thrive on the static information compiled into all classes. Using reflection, they can analyze the requested class and figure out which Dependencies are needed.

如第 4.2 节所述,构造函数注入是应用 DI 的首选方式,因此,所有DI 容器天生就理解构造函数注入。具体来说,他们通过将自己的配置与从类的类型信息中提取的信息相结合来组成对象图。这称为自动接线

As explained in section 4.2, Constructor Injection is the preferred way of applying DI and, because of this, all DI Containers inherently understand Constructor Injection. Specifically, they compose object graphs by combining their own configuration with the information extracted from the classes’ type information. This is called Auto-Wiring.

大多数DI 容器也理解Property Injection,尽管有些需要您明确启用它。考虑到属性注入的缺点(如 4.4 节所述),这是一件好事。图 12.2描述了大多数DI 容器自动连接对象图所遵循的一般算法。

Most DI Containers also understand Property Injection, although some require you to explicitly enable it. Considering the downsides of Property Injection (as explained in section 4.4), this is a good thing. Figure 12.2 describes the general algorithm most DI Containers follow to Auto-Wire an object graph.

12-02.eps

图 12.2自动装配的 简化工作流程。DI 容器使用其配置来查找与请求类型匹配的适当具体类。然后它使用反射来检查类的构造函数。

Figure 12.2 Simplified workflow for Auto-Wiring. A DI Container uses its configuration to find the appropriate concrete class that matches the requested type. It then uses reflection to examine the class’s constructor.

如图所示,DI 容器为请求的抽象找到具体类型。如果具体类型的构造函数需要参数,则递归过程开始,DI 容器在其中为每个参数类型重复该过程,直到满足所有构造函数参数。完成后,容器会构造具体类型,同时注入递归解析的Dependencies

As shown, a DI Container finds the concrete type for a requested Abstraction. If the constructor of the concrete type requires arguments, a recursive process starts where the DI Container repeats the process for each argument type until all constructor arguments are satisfied. When this is complete, the container constructs the concrete type while injecting the recursively resolved Dependencies.

在 12.2 节中,我们将仔细研究如何配置容器。现在,最重要的是要理解配置的核心是抽象与其表示的具体类之间的映射列表。这听起来有点理论化,所以我们认为一个例子会有所帮助。

In section 12.2, we’ll take a closer look at how containers can be configured. For now, the most important thing to understand is that at the core of the configuration is a list of mappings between Abstractions and their represented concrete classes. That sounds a bit theoretical, so we think an example will be helpful.

12.1.3 示例:实现一个支持自动装配的简单DI 容器

12.1.3 Example: Implementing a simplistic DI Container that supports Auto-Wiring

演示如何自动接线有效,并且为了表明DI Containers没有什么神奇之处,让我们看一个简单的DI Container实现,它能够使用Auto-Wiring构建复杂的对象图。

To demonstrate how Auto-Wiring works, and to show that there’s nothing magical about DI Containers, let’s look at a simplistic DI Container implementation that’s able to build complex object graphs using Auto-Wiring.

清单 12.1显示了这个简单的DI 容器实现。它不支持Lifetime ManagementInterception或许多其他重要功能。唯一支持的功能是Auto-Wiring

Listing 12.1 shows this simplistic DI Container implementation. It doesn’t support Lifetime Management, Interception, or many other important features. The only supported feature is Auto-Wiring.

坏.tif

清单 12.1支持自动装配 的简单DI 容器

Listing 12.1 A simplistic DI Container that supports Auto-Wiring

public class AutoWireContainer
{
    Dictionary<Type, Func<object>> registrations =    ①  
        new Dictionary<Type, Func<object>>();

    public void Register(
        Type serviceType, Type componentType)    ②  
    {    ②  
        this.registrations[serviceType] =    ②  
            () => this.CreateNew(componentType);    ②  
    }    ②  

    public void Register(    ③  
        Type serviceType, Func<object> factory)    ③  
    {    ③  
        this.registrations[serviceType] = factory;    ③  
    }    ③  

    public object Resolve(Type type)    ④  
    {    ④  
        if(this.registrations.ContainsKey(type))    ④  
        {    ④  
            return this.registrations[type]();    ④  
        }    ④  
    ④  
        throw new InvalidOperationException(    ④  
            "No registration for " + type);    ④  
    }

    private object CreateNew(Type componentType)    ⑤  
    {    ⑤  
        var ctor =    ⑤  
            componentType.GetConstructors()[0];    ⑤  
    ⑤  
        var dependencies =    ⑤  
            from p in ctor.GetParameters()    ⑤  
            select this.Resolve(p.ParameterType);    ⑤  
    ⑤  
        return Activator.CreateInstance(    ⑤  
            componentType, dependencies.ToArray());  ⑤  
    }
}

包含一组注册。注册抽象(服务类型)和组件类型之间的映射。Abstraction被表示为字典的键,而它的值是一个委托,允许构造一个实现Abstraction的组件的新实例。方法_AutoWireContainerFunc<object>Register通过告诉容器应该为给定的服务类型创建哪个组件来注册一个新的注册。您只需指定要创建的组件,而不是如何创建。

The AutoWireContainer contains a set of registrations. A registration is a mapping between an Abstraction (the service type) and a component type. The Abstraction is presented as the dictionary’s key, whereas its value is a Func<object> delegate that allows constructing a new instance of a component that implements the Abstraction. The Register method registers a new registration by telling the container which component should be created for a given service type. You only specify which component to create, not how.

Register方法将服务类型的映射添加到registrations字典中。可选地,该Register方法可以直接为容器提供Func<T>委托。这绕过了它的自动装配能力。它将改为调用提供的委托。

The Register method adds the mapping for the service type to the registrations dictionary. Optionally, the Register method can supply the container with a Func<T> delegate directly. This bypasses its Auto-Wiring abilities. It will call the supplied delegate instead.

这些Resolve方法允许解析完整的对象图。它Func<T>registrations字典中获取请求的serviceType,调用它,并返回它的值。如果请求的类型没有注册,则Resolve抛出异常。最后,CreateNew通过遍历组件的构造函数参数并递归地回调容器来创建组件的新实例。它通过调用Resolve每个参数,同时提供参数的Type. 当所有类型的依赖项都以这种方式解析时,它通过使用反射(使用System.Activator).

The Resolve methods allows resolving a complete object graph. It gets the Func<T> from the registrations dictionary for the requested serviceType, invokes it, and returns its value. In case there’s no registration for the requested type, Resolve throws an exception. And finally, CreateNew creates a new instance of a component by iterating over the component’s constructor parameters and calling back into the container recursively. It does so by calling Resolve for each parameter, while supplying the parameter’s Type. When all the type’s Dependencies are resolved in this way, it constructs the type itself by using reflection (using the System.Activator class).

实例AutoWireContainer可以配置为组成任意对象图。回到清单 3.13 的第 3 章,您HomeController使用Pure DI创建了一个。下一个清单重复第 3 章中的清单。我们将使用它作为示例来演示之前在 中定义的自动装配功能AutoWireContainer

An AutoWireContainer instance can be configured to compose arbitrary object graphs. Back in chapter 3 in listing 3.13, you created a HomeController using Pure DI. The next listing repeats that listing from chapter 3. We’ll use that as an example to demonstrate the Auto-Wiring capabilities previously defined in AutoWireContainer.

清单 12.2HomeController为使用Pure DI 组合一个对象图

Listing 12.2 Composing an object graph for HomeController using Pure DI

new HomeController(
    new ProductService(
        new SqlProductRepository(
            new CommerceContext(connectionString)),
        new AspNetUserContextAdapter()));

AutoWireContainer您可以使用来注册五个必需的组件,而不是像前面的清单那样手动组合这个对象图。为此,您必须将这五个组件映射到它们适当的抽象表 12.1列出了这些映射。

Instead of composing this object graph by hand, as done in the previous listing, you can use the AutoWireContainer to register the five required components. To do this, you must map these five components to their appropriate Abstraction. Table 12.1 lists these mappings.

表 12.1 支持自动装配的映射类型HomeController
抽象具体类型
HomeControllerHomeController
IProductServiceProductService
IProductRepositorySqlProductRepository
CommerceContextCommerceContext
IUserContextAspNetUserContextAdapter

清单 12.3展示了如何使用AutoWireContainerRegister方法添加表 12.1中指定的所需映射。请注意,此清单使用Configuration as Code。我们将在 12.2.2 节中讨论配置即代码

Listing 12.3 shows how you can use the AutoWireContainer’s Register methods to add the required mappings specified in table 12.1. Note that this listing uses Configuration as Code. We’ll discuss Configuration as Code in section 12.2.2.

清单 12.3 使用注册AutoWireContainerHomeController

Listing 12.3 Using AutoWireContainer to register HomeController

var container = new AutoWireContainer();    ①  

container.Register(    ②  
    typeof(IUserContext),    ②  
    typeof(AspNetUserContextAdapter));    ②  
    ②  
container.Register(    ②  
    typeof(IProductRepository),    ②  
    typeof(SqlProductRepository));    ②  
    ②  
container.Register(    ②  
    typeof(IProductService),    ②  
    typeof(ProductService));    ②  

container.Register(    ③  
    typeof(HomeController),    ③  
    typeof(HomeController));    ③  

container.Register(    ④  
    typeof(CommerceContext),    ④  
    () => new CommerceContext(connectionString));    ④  

您可能会发现表 12.1清单 12.3HomeController中的映射令人困惑,因为它映射到自身而不是映射到抽象。然而,这是一种常见的做法,尤其是在处理位于对象图顶部的类型时,例如 MVC 控制器。

You might find the mapping for HomeController in table 12.1 and listing 12.3 confusing, because it maps to itself instead of mapping to an Abstraction. This is a common practice, however, especially when dealing with types that are at the top of the object graph, such as MVC controllers.

您在清单 4.4、7.8 和 8.3 中看到了类似的东西,其中您在请求类型HomeController时创建了一个新实例。HomeController这些清单与清单 12.3之间的主要区别在于后者使用DI Container而不是Pure DI

You saw something similar in listings 4.4, 7.8, and 8.3, where you created a new HomeController instance when a HomeController type was requested. The main difference between those listings and listing 12.3 is that the latter uses a DI Container instead of Pure DI.

清单 12.3有效地注册了组成对象图所需的所有组件HomeController。您现在可以使用配置AutoWireContainer来创建一个新的HomeController.

Listing 12.3 effectively registered all components required for the composition of an object graph of HomeController. You can now use the configured AutoWireContainer to create a new HomeController.

清单 12.4 用于AutoWireContainer解析 aHomeController

Listing 12.4 Using AutoWireContainer to resolve a HomeController

object controller = container.Resolve(typeof(HomeController));

AutoWireContainerResolve方法被调用以请求新HomeController类型,容器将递归调用自身,直到它解决了所有需要的Dependencies。在此之后,创建一个新HomeController实例,同时将已解析的依赖项提供给其构造函数。图 12.3显示了递归过程,使用了一些非常规的表示来可视化递归调用。该container实例分布在四个独立的垂直时间线上。因为有多层递归调用,将它们折叠成一行,就像 UML 序列图的规范一样,会非常混乱。

When the AutoWireContainer’s Resolve method is called to request a new HomeController type, the container will call itself recursively until it has resolved all of its required Dependencies. After this, a new HomeController instance is created, while supplying the resolved Dependencies to its constructor. Figure 12.3 shows the recursive process, using a somewhat unconventional representation to visualize recursive calls. The container instance is spread out over four separate vertical time lines. Because there are multiple levels of recursive calls, folding them into one single line, as is the norm with UML sequence diagrams, would be quite confusing.

12-03.eps

图 12.3 Composition RootHomeController从 the 请求a container,它递归地回调到自身以请求HomeControllerDependencies

Figure 12.3 The Composition Root requests a HomeController from the container, which recursively calls back into itself to request HomeController’s Dependencies.

DI 容器收到对 a 的请求时HomeController,它要做的第一件事是在其配置中查找类型。HomeController是一个具体类,您将其映射到自身。然后容器使用反射来检查HomeController具有以下签名的唯一构造函数:

When the DI Container receives a request for a HomeController, the first thing it’ll do is look up the type in its configuration. HomeController is a concrete class, which you mapped to itself. The container then uses reflection to inspect HomeController’s one and only constructor with the following signature:

public HomeController(IProductService productService)

由于此构造函数不是无参数构造函数,因此在按照图 12.2IProductService中的一般流程图进行操作时,需要重复构造函数参数的过程。容器在其配置中查找并发现它映射到具体类。的单个公共构造函数具有以下签名:IProductServiceProductServiceProductService

Because this constructor isn’t a parameterless constructor, it needs to repeat the process for the IProductService constructor argument when following the general flowchart from figure 12.2. The container looks up IProductService in its configuration and finds that it maps to the concrete ProductService class. The single public constructor for ProductService has this signature:

public ProductService(
    IProductRepository repository,
    IUserContext userContext)

那仍然不是无参数构造函数,现在有两个构造函数参数需要处理。容器按顺序处理每一个,所以它从IProductRepository接口开始根据配置,映射到SqlProductRepository. 它SqlProductRepository有一个带有此签名的公共构造函数:

That’s still not a parameterless constructor, and now there are two constructor arguments to deal with. The container takes care of each in order, so it starts with the IProductRepository interface that, according to the configuration, maps to SqlProductRepository. That SqlProductRepository has a public constructor with this signature:

public SqlProductRepository(CommerceContext context)

这又不是无参数构造函数,因此容器需要解析CommerceContext以满足SqlProductRepository的构造函数。CommerceContext但是,在清单 12.3中使用以下委托注册:

That’s again not a parameterless constructor, so the container needs to resolve CommerceContext to satisfy SqlProductRepository’s constructor. CommerceContext, however, is registered in listing 12.3 using the following delegate:

() => new CommerceContext(connectionString)    ①  

容器调用该委托,这会产生一个新CommerceContext实例。这一次,没有使用自动接线

The container calls that delegate, which results in a new CommerceContext instance. This time, no Auto-Wiring is used.

现在容器具有适当的值CommerceContext,它可以调用构造函数。它现在已经成功地处理了构造函数的 Repository 参数,但它需要保留该值一段时间;它还需要处理的构造函数参数。根据配置,映射到具体类SqlProductRepositoryProductServiceProductServiceuserContextIUserContextAspNetUserContextAdapter,它有这个公共构造函数:

Now that the container has the appropriate value for CommerceContext, it can invoke the SqlProductRepository constructor. It has now successfully handled the Repository parameter for the ProductService constructor, but it’ll need to hold on to that value for a while longer; it also needs to take care of ProductService’s userContext constructor parameter. According to the configuration, IUserContext maps to the concrete AspNetUserContextAdapter class, which has this public constructor:

public AspNetUserContextAdapter()

因为AspNetUserContextAdapter包含无参数构造函数,所以无需解析任何Dependencies即可创建它。它现在可以将新AspNetUserContextAdapter实例传递给ProductService构造函数。与SqlProductRepository之前的一起,它现在实现了ProductService构造函数并通过反射调用它。最后,它将新创建ProductService的实例传递给HomeController构造函数并返回该HomeController实例。图 12.4显示了图 12.2中的一般工作流程如何映射到AutoWireContainer清单12.1

Because AspNetUserContextAdapter contains a parameterless constructor, it can be created without having to resolve any Dependencies. It can now pass the new AspNetUserContextAdapter instance to the ProductService constructor. Together with the SqlProductRepository from before, it now fulfills the ProductService constructor and invokes it via reflection. Finally, it passes the newly created ProductService instance to the HomeController constructor and returns the HomeController instance. Figure 12.4 shows how the general workflow presented in figure 12.2 maps to the AutoWireContainer from listing 12.1.

12-04.eps

图 12.4 Auto-Wiring的 简化工作流程映射到清单 12.1中的代码。查询字典以registrations获取具体类型,解析其构造函数参数,并使用其解析的Dependencies创建具体类型。

Figure 12.4 Simplified workflow for Auto-Wiring mapped to the code from listing 12.1. The registrations dictionary is queried for the concrete type, its constructor parameters get resolved, and the concrete type is created using its resolved Dependencies.

使用DI ContainerAuto-Wiring的好处使用清单 12.3中所示的功能而不是使用清单 12.2中所示的Pure DI的不同之处在于,使用Pure DI时,对组件构造函数的任何更改都需要反映在Composition Root中。另一方面,Auto-Wiring使Composition Root对此类更改更具弹性。

The advantage of using a DI Container’s Auto-Wiring capabilities as shown in listing 12.3 rather than using Pure DI as shown in listing 12.2 is that with Pure DI, any change to a component’s constructor needs to be reflected in the Composition Root. Auto-Wiring, on the other hand, makes the Composition Root more resilient to such changes.

例如,假设您需要添加一个CommerceContext 依赖项,以便它查询数据库。以下清单显示了在应用Pure DI时需要对Composition Root进行的更改。AspNetUserContextAdapter

For example, let’s say you need to add a CommerceContext Dependency to AspNetUserContextAdapter in order for it to query the database. The following listing shows the change that needs to be made to the Composition Root when you apply Pure DI.

清单 12.5 更改后的组合根AspNetUserContextAdapter

Listing 12.5 Composition Root for the changed AspNetUserContextAdapter

new HomeController(
    new ProductService(
        new SqlProductRepository(
            new CommerceContext(connectionString)),
        new AspNetUserContextAdapter(
            new CommerceContext(connectionString))));    ①  

另一方面,使用Auto-Wiring时,在这种情况下不需要更改Composition Root 。AspNetUserContextAdapterAuto-Wired,并且因为它的新CommerceContext Dependency已经注册,容器将能够满足新的构造函数参数,并会愉快地构造一个新的AspNetUserContextAdapter.

With Auto-Wiring, on the other hand, no changes to the Composition Root are required in this case. AspNetUserContextAdapter is Auto-Wired, and because its new CommerceContext Dependency was already registered, the container will be able to satisfy the new constructor argument and will happily construct a new AspNetUserContextAdapter.

这就是自动装配的工作方式,尽管DI 容器还需要处理生命周期管理,也许还需要解决属性注入以及其他更专业的创建需求。

This is how Auto-Wiring works, although DI Containers also need to take care of Lifetime Management and, perhaps, address Property Injection as well as other, more specialized, creational requirements.

重点是构造函数注入静态地公布了一个类的依赖要求,DI 容器使用该信息来自动连接复杂的对象图。必须先配置容器,然后才能组成对象图。组件的注册可以通过多种方式完成。

The salient point is that Constructor Injection statically advertises the Dependency requirements of a class, and DI Containers use that information to Auto-Wire complex object graphs. A container must be configured before it can compose object graphs. Registration of components can be done in various ways.

12.2 配置DI 容器

12.2 Configuring DI Containers

虽然该Resolve方法是大部分操作发生的地方,但您应该期望将大部分时间花在DI Container的配置 API 上。毕竟,解析对象图是一个单一的方法调用。

Although the Resolve method is where most of the action happens, you should expect to spend most of your time with a DI Container’s configuration API. Resolving object graphs is, after all, a single method call.

12-05.eps

图 12.5针对显式维度和绑定程度显示的配置DI 容器 的最常用方法

Figure 12.5 The most common ways to configure a DI Container shown against dimensions of explicitness and the degree of binding

DI 容器倾向于支持两个或三个常见的配置选项,如图 12.5所示。有些不支持配置文件,有些也不支持自动注册,而配置即代码支持无处不在。大多数允许您在同一个应用程序中混合使用多种方法。第 12.2.4 节讨论了为什么要使用混合方法。

DI Containers tend to support two or three of the common configuration options shown in figure 12.5. Some don’t support configuration files, and others also lack support for Auto-Registration, whereas Configuration as Code support is ubiquitous. Most allow you to mix several approaches in the same application. Section 12.2.4 discusses why you’d want to use a mixed approach.

这三个配置选项具有不同的特性,这使得它们在不同的情况下很有用。配置文件和配置即代码都倾向于显式,因为它们需要您单独注册每个组件。另一方面,自动注册更隐含,因为它使用约定通过单个规则注册一组组件。

These three configuration options have different characteristics that make them useful in different situations. Both configuration files and Configuration as Code tend to be explicit, because they require you to register each component individually. Auto-Registration, on the other hand, is more implicit because it uses conventions to register a set of components by a single rule.

当您使用Configuration as Code时,您将容器配置编译到程序集中,而基于文件的配置使您能够支持后期绑定,您可以在其中更改配置而无需重新编译应用程序。在该维度中,自动注册处于中间位置,因为您可以要求它扫描编译时已知的单个程序集,或者扫描编译时可能未知的预定义文件夹中的所有程序集。表 12.2列出了每个选项的优点和缺点。

When you use Configuration as Code, you compile the container configuration into an assembly, whereas file-based configuration enables you to support late binding, where you can change the configuration without recompiling the application. In that dimension, Auto-Registration falls somewhere in the middle, because you can ask it to scan a single assembly known at compile time or, alternatively, to scan all assemblies in a predefined folder that might be unknown at compile time. Table 12.2 lists the advantages and disadvantages of each option.

表 12.2 配置选项
风格描述优点缺点
配置文件映射在配置文件中指定(通常为 XML 或 JSON 格式)
  • 支持替换无需重新编译
  • Supports replacement without recompilation
  • 没有编译时检查
  • No compile-time checks
  • 冗长而脆弱
  • Verbose and brittle
配置即代码代码明确确定映射
  • 编译时检查
  • Compile-time checks
  • 高度可控
  • High degree of control
  • 不支持不重新编译的替换
  • No support for replacement without recompilation
自动注册规则用于使用反射定位合适的组件并构建映射。
  • 支持替换无需重新编译
  • Supports replacement without recompilation
  • 需要更少的努力
  • Less effort required
  • 帮助执行约定以使代码库更加一致
  • Helps enforce conventions to make a code base more consistent
  • 没有编译时检查
  • No compile-time checks
  • 更少的控制
  • Less control
  • 一开始可能看起来更抽象
  • May seem more abstract at first

从历史上看,DI 容器从配置文件开始,这也解释了为什么旧库仍然支持它。但是这个特性被淡化了,取而代之的是更传统的方法。这就是为什么最近开发的DI 容器(例如 Simple Injector 和 Microsoft.Extensions.DependencyInjection)没有对基于文件的配置的任何内置支持。

Historically, DI Containers started out with configuration files, which also explains why the older libraries still support this. But this feature has been downplayed in favor of more conventional approaches. That’s why more recently developed DI Containers, such as Simple Injector and Microsoft.Extensions.DependencyInjection, don’t have any built-in support for file-based configuration.

尽管自动注册是最现代的选择,但它并不是最明显的起点。由于它的隐含性,它可能看起来比更明确的选项更抽象,因此,我们将按历史顺序介绍每个选项,从配置文件开始。

Although Auto-Registration is the most modern option, it isn’t the most obvious place to start. Because of its implicitness, it may seem more abstract than the more explicit options, so instead, we’ll cover each option in historical order, starting with configuration files.

12.2.1 使用配置文件配置容器

12.2.1 Configuring containers with configuration files

DI 容器在 2000 年代初首次出现时,它们都使用 XML 作为配置机制——当时大多数东西都是这样做的。后来将 XML 作为配置机制的经验表明,这很少是最佳选择。

When DI Containers first appeared back in the early 2000s, they all used XML as a configuration mechanism — most things did back then. Experience with XML as a configuration mechanism later revealed that this is rarely the best option.

XML 往往冗长而脆弱。当您在 XML 中配置DI 容器时,您会识别各种类和接口,但如果您拼错某些内容,则没有编译器支持来警告您。即使类名正确,也不能保证所需的程序集将位于应用程序的探测路径中。

XML tends to be verbose and brittle. When you configure a DI Container in XML, you identify various classes and interfaces, but you have no compiler support to warn you if you misspell something. Even if the class names are correct, there’s no guarantee that the required assembly is going to be in the application’s probing path.

雪上加霜的是,与纯代码相比,XML 的表达能力有限。这有时会导致很难或不可能在配置文件中表达某些配置,而这些配置在其他情况下用代码表达是微不足道的。例如,在清单 12.3CommerceContext中,您使用 lambda 表达式注册了。这样的 lambda 表达式既不能用 XML 也不能用 JSON 表示。

To add insult to injury, the expressiveness of XML is limited compared to that of plain code. This sometimes makes it hard or impossible to express certain configurations in a configuration file that are otherwise trivial to express in code. In listing 12.3, for instance, you registered the CommerceContext using a lambda expression. Such a lambda expression can be expressed in neither XML nor JSON.

另一方面,配置文件的优点是无需重新编译即可更改应用程序的行为。如果您开发的软件交付给成千上万的客户,这就很有价值,因为它为他们提供了一种自定义方式应用程序。但是,如果您编写一个内部应用程序或您控制部署环境的网站,那么当您需要更改行为时,重新编译和重新部署应用程序通常会更容易。

The advantage of configuration files, on the other hand, is that you can change the behavior of the application without recompilation. This is valuable if you develop software that ships to thousands of customers, because it gives them a way to customize the application. But if you write an internal application or a website where you control the deployment environment, it’s often easier to recompile and redeploy the application when you need to change the behavior.

DI 容器通常通过将其指向特定的配置文件来配置文件。以下示例以 Autofac 为例。

A DI Container is often configured with files by pointing it to a particular configuration file. The following example uses Autofac as an example.

在此示例中,您将配置与 12.1.3 节中相同的类。大部分任务是应用表 12.1中概述的配置,但您还必须提供类似的配置以支持HomeController类的组合。以下清单显示了启动和运行应用程序所需的配置。

In this example, you’ll configure the same classes as in section 12.1.3. A large part of the task is to apply the configuration outlined in table 12.1, but you must also supply a similar configuration to support composition of the HomeController class. The following listing shows the configuration necessary to get the application up and running.

清单 12.6 使用 JSON 配置文件配置 Autofac

Listing 12.6 Configuring Autofac with a JSON configuration file

{
  "defaultAssembly": "Commerce.Web",    ①  
  "components": [
  {
    "services": [{    ②  
      "type":    ②  
        "Commerce.Domain.IUserContext, Commerce.Domain"  ②  
    }],    ②  
    "type":    ②  
      "Commerce.Web.AspNetUserContextAdapter"    ②  
  },
  {
    "services": [{
      "type": "Commerce.Domain.IProductRepository, Commerce.Domain"
    }],
    "type": "Commerce.SqlDataAccess.SqlProductRepository, Commerce.
      SqlDataAccess"
  },
  {
    "services": [{
      "type": "Commerce.Domain.IProductService, Commerce.Domain"
    }],
    "type":
      "Commerce.Domain.ProductService, Commerce.Domain"
  },
  {
    "type": "Commerce.Web.Controllers.HomeController"    ③  
  },
  {
    "type": "Commerce.SqlDataAccess.CommerceContext,  ④  
      Commerce.SqlDataAccess",    ④  
    "parameters": {    ④  
      "connectionString":    ④  
        "Server=.;Database=MaryCommerce;Trusted_  ④  
      Connection=True;"    ④  
    }
  }]
}

在此示例中,如果您未在类型或接口引用中指定程序集限定的类型名称,则将假定为默认程序集。对于简单映射,必须使用完整的类型名称,包括命名空间和程序集名称。因为排除了程序集的名称,所以Autofac在程序集中查找defaultAssemblyAspNetUserContextAdapterCommerce.Web,您将其定义为defaultAssembly

In this example, if you don’t specify an assembly-qualified type name in a type or interface reference, defaultAssembly will be assumed to be the default assembly. For a simple mapping, full type names must be used, including namespace and assembly name. Because AspNetUserContextAdapter excluded the name of the assembly, Autofac looks for it in the Commerce.Web assembly, which you defined as the defaultAssembly.

即使从这个简单的代码清单中也可以看出,JSON 配置往往非常冗长。简单的映射,例如从IUserContext接口到AspNetUserContextAdapter类的映射需要大量以方括号和完全限定的类型名称形式出现的文本。

As you can see from even this simple code listing, JSON configuration tends to be quite verbose. Simple mappings like the one from the IUserContext interface to the AspNetUserContextAdapter class require quite a lot of text in the form of brackets and fully qualified type names.

您可能还记得,CommerceContext将连接字符串作为输入,因此您需要指定如何找到该字符串的值。通过添加parameters到映射,您可以通过参数名称指定值——在本例中为connectionString. 使用以下代码将配置加载到容器中。

As you may recall, CommerceContext takes a connection string as input, so you need to specify how the value of this string is found. By adding parameters to a mapping, you can specify values by their parameter name — in this case, connectionString. Loading the configuration into the container is done with the following code.

清单 12.7 使用 Autofac 读取配置文件

Listing 12.7 Reading configuration files using Autofac

var builder = new Autofac.ContainerBuilder();    ①  

IConfigurationRoot configuration =    ②  
    new ConfigurationBuilder()    ②  
    .AddJsonFile("autofac.json")    ②  
    .Build();    ②  

builder.RegisterModule(    ③  
    new Autofac.Configuration.ConfigurationModule(  ③  
        configuration));    ③  

Autofac 是本书中包含的唯一支持配置文件的DI容器,但还有其他此处未涵盖的DI 容器继续支持配置文件。每个容器的确切模式不同,但整体结构往往相似,因为您需要将抽象映射到实现。

Autofac is the only DI Container included in this book that supports configuration files, but there are other DI Containers not covered here that continue to support configuration files. The exact schema is different for each container, but the overall structure tends to be similar, because you need to map an Abstraction to an implementation.

不要让不支持处理配置文件过多地影响您对DI 容器的选择。如前所述,只有真正的后期绑定组件才应该在配置文件中定义,这不太可能超过少数。即使没有容器的支持,也可以通过几个简单的语句从配置文件中加载类型,如清单 1.2 所示。

Don’t let the absence of support for handling configuration files influence your choice of a DI Container too much. As described previously, only true late-bound components should be defined in configuration files, which will unlikely be more than a handful. Even with absence of support from your container, types can be loaded from configuration files in a few simple statements, as shown in listing 1.2.

由于冗长和脆弱的缺点,您应该更喜欢其他配置容器的方法。Configuration as Code在粒度和概念上类似于配置文件,但显然使用代码而不是配置文件。

Because of the disadvantages of verbosity and brittleness, you should prefer the other alternatives for configuring containers. Configuration as Code is similar to configuration files in granularity and concept, but obviously uses code instead of configuration files.

12.2.2 使用配置即代码配置容器

12.2.2 Configuring containers using Configuration as Code

也许编写应用程序的最简单方法是对对象图的构造进行硬编码。这似乎违背了 DI 的整体精神,因为它决定了在编译时应该用于所有抽象的具体实现。但如果在Composition Root中完成,它只会违反表 1.1 中列出的好处之一,即后期绑定。

Perhaps the easiest way to compose an application is to hard code the construction of object graphs. This may seem to go against the whole spirit of DI, because it determines the concrete implementations that should be used for all Abstractions at compile time. But if done in a Composition Root, it only violates one of the benefits listed in table 1.1, namely, late binding.

后期绑定的好处如果丢失Dependencies是硬编码的,它就会丢失,但是,正如我们在第 1 章中提到的,这可能并不适用于所有类型的应用程序。如果您的应用程序在受控环境中部署在有限数量的实例中,则在需要替换模块时可以更轻松地重新编译和重新部署应用程序:

The benefit of late binding is lost if Dependencies are hard-coded, but, as we mentioned in chapter 1, this may not be relevant for all types of applications. If your application is deployed in a limited number of instances in a controlled environment, it can be easier to recompile and redeploy the application if you need to replace modules:

我经常认为人们过于急于定义配置文件。通常,一种编程语言会提供一种简单而强大的配置机制。4个 

I often think that people are over-eager to define configuration files. Often a programming language makes a straightforward and powerful configuration mechanism.4 

马丁福勒

Martin Fowler

当您使用Configuration as Code时,您明确声明了与使用配置文件时相同的离散映射——只是您使用代码而不是 XML 或 JSON。

When you use Configuration as Code, you explicitly state the same discrete mappings as when you use configuration files — only you use code instead of XML or JSON.

所有现代DI 容器都完全支持配置即代码作为配置文件的继承者;事实上,他们中的大多数将此作为默认机制,配置文件作为可选功能。如前所述,有些甚至根本不提供对配置文件的支持。为支持配置即代码而公开的 API因DI ContainerDI Container 而异,但总体目标仍然是定义抽象和具体类型之间的离散映射。

All modern DI Containers fully support Configuration as Code as the successor to configuration files; in fact, most of them present this as the default mechanism, with configuration files as an optional feature. As stated previously, some don’t even offer support for configuration files at all. The API exposed to support Configuration as Code differs from DI Container to DI Container, but the overall goal is still to define discrete mappings between Abstractions and concrete types.

让我们来看看如何使用配置即代码来配置电子商务应用程序与 Microsoft.Extensions.DependencyInjection。为此,我们将使用一个示例,该示例使用代码配置示例电子商务应用程序。

Let’s take a look how to configure the e-commerce application using Configuration as Code with Microsoft.Extensions.DependencyInjection. For this, we’ll use an example that configures the sample e-commerce application with code.

在 12.2.1 节中,您了解了如何使用 Autofac 使用配置文件配置示例电子商务应用程序。我们还可以使用 Autofac演示配置即代码,但是,为了使本章更有趣,我们将在本示例中使用 Microsoft.Extensions.DependencyInjection。使用 Microsoft 的配置 API,您可以更简洁地表达清单 12.6中的配置,如下所示。

In section 12.2.1, you saw how to configure the sample e-commerce application with configuration files using Autofac. We could also demonstrate Configuration as Code with Autofac, but, to make this chapter a bit more interesting, we’ll instead use Microsoft.Extensions.DependencyInjection in this example. Using Microsoft’s configuration API, you can express the configuration from listing 12.6 more compactly, as shown here.

清单 12.8 配置 Microsoft.Extensions.DependencyInjection带代码

Listing 12.8 Configuring Microsoft.Extensions.DependencyInjection with code

var services = new ServiceCollection();    ①  

services.AddSingleton<    ②  
    IUserContext,    ②  
    AspNetUserContextAdapter>();    ②  

services.AddTransient<
    IProductRepository,
    SqlProductRepository>();

services.AddTransient<
    IProductService,
    ProductService>();

services.AddTransient<HomeController>();    ③  

services.AddScoped<CommerceContext>(    ④  
    p => new CommerceContext(connectionString));    ④  

ServiceCollection是 Microsoft 的 Autofac 的等价物ContainerBuilder,它定义了抽象和实现之间的映射。、和方法用于在抽象和具体类型之间为其特定的Lifestyle添加自动连线映射。这些方法是通用的,这会产生更简洁的代码,并带来一些额外的编译时检查的额外好处。如果具体类型映射到自身,而不是将抽象映射到具体类型,则有一个方便的重载,它只将具体类型作为泛型类型参数。并且,就像清单 12.1的例子一样AddTransientAddScopedAddSingletonAutoWireContainer,此DI 容器的 API包含允许将抽象映射到Func<T>委托的重载。

ServiceCollection is Microsoft’s equivalent to Autofac’s ContainerBuilder, which defines the mappings between Abstractions and implementations. The AddTransient, AddScoped, and AddSingleton methods are used to add Auto-Wired mappings between Abstractions and concrete types for their specific Lifestyle. These methods are generic, which results in more condensed code with the additional benefit of getting some extra compile-time checking. In case a concrete type maps to itself, instead of having an Abstraction mapping to a concrete type, there’s a convenient overload that just takes in the concrete type as a generic type argument. And, just as with the AutoWireContainer example of listing 12.1, the API of this DI Container contains an overload that allows mapping an Abstraction to a Func<T> delegate.

清单 12.8中,我们冒昧地演示了使用三种常见生活方式的组件注册:SingletonTransientScoped。以下章节将更详细地展示如何为每个容器配置生活方式。

In listing 12.8, we took the liberty of demonstrating the registration of components using the three common lifestyles: Singleton, Transient, and Scoped. The following chapters show how to configure lifestyles for each container in more detail.

将此代码与清单 12.6进行比较,注意它的紧凑程度——尽管它做的事情完全相同。像 from IProductServiceto这样的简单映射ProductService是用单个方法调用表示的。

Compare this code with listing 12.6, and notice how much more compact it is — even though it does the exact same thing. A simple mapping like the one from IProductService to ProductService is expressed with a single method call.

配置即代码不仅比配置文件中表达的配置更紧凑,而且还享有编译器支持。清单 12.8中使用的类型参数表示编译器检查的实际类型。泛型更进一步,因为使用泛型类型约束(例如 Microsoft 的 API 应用)允许编译器检查提供的具体类型是否与抽象相匹配。如果无法进行转换,则代码将无法编译。

Not only is Configuration as Code much more compact than configurations expressed in a configuration file, it also enjoys compiler support. The type arguments used in listing 12.8 represent real types that the compiler checks. Generics go even a step further, because the use of generic type constraints such as Microsoft’s API applies allows the compiler to check whether the supplied concrete type matches the Abstraction. If a conversion isn’t possible, the code won’t compile.

虽然配置即代码安全且易于使用,但它仍然需要比您想象的更多的维护。每次向应用程序添加新类型时,您还必须记住对其进行注册——许多注册最终都是相似的。自动注册解决了这个问题。

Although Configuration as Code is safe and easy to use, it still requires more maintenance than you might like. Every time you add a new type to an application, you must also remember to register it — and many registrations end up being similar. Auto-Registration addresses this issue.

12.2.3 使用自动注册按照惯例配置容器

12.2.3 Configuring containers by convention using Auto-Registration

考虑到清单 12.8的注册,在您的项目中使用这几行代码可能完全没问题。然而,当项目增长时,设置DI 容器所需的注册量也会增长。随着时间的推移,您可能会看到许多类似的注册出现。他们通常会遵循一个共同的模式。以下清单显示了这些注册如何开始看起来有些重复。

Considering the registrations of listing 12.8, it might be completely fine to have these few lines of code in your project. When a project grows, however, so will the amount of registrations required to set up the DI Container. In time, you’re likely to see many similar registrations appear. They’ll typically follow a common pattern. The following listing shows how these registrations can start to look somewhat repetitive.

清单 12.9 使用配置即代码时的重复注册

Listing 12.9 Repetition in registrations when using Configuration as Code

services.AddTransient<IProductRepository, SqlProductRepository>();
services.AddTransient<ICustomerRepository, SqlCustomerRepository>();
services.AddTransient<IOrderRepository, SqlOrderRepository>();
services.AddTransient<IShipmentRepository, SqlShipmentRepository>();
services.AddTransient<IImageRepository, SqlImageRepository>();

services.AddTransient<IProductService, ProductService>();
services.AddTransient<ICustomerService, CustomerService>();
services.AddTransient<IOrderService, OrderService>();
services.AddTransient<IShipmentService, ShipmentService>();
services.AddTransient<IImageService, ImageService>();

这样重复写注册码就违反了DRY原则。它也似乎是一段无用的基础架构代码,不会为应用程序增加太多价值。如果您可以自动注册组件,您可以节省时间并减少错误,假设这些组件遵循某种约定。许多DI 容器提供自动注册功能,让您可以引入自己的约定并应用Convention over Configuration

Repeatedly writing registration code like that violates the DRY principle. It also seems like an unproductive piece of infrastructure code that doesn’t add much value to the application. You can save time and make fewer errors if you can automate the registration of components, assuming those components follow some sort of convention. Many DI Containers provide Auto-Registration capabilities that let you introduce your own conventions and apply Convention over Configuration.

实际上,您可能需要将自动注册配置即代码或配置文件相结合,因为您可能无法将每个组件都纳入一个有意义的约定中。但是,您越能将代码库移向约定,它就越容易维护。

In reality, you may need to combine Auto-Registration with Configuration as Code or configuration files, because you may not be able to fit every single component into a meaningful convention. But the more you can move your code base towards conventions, the more maintainable it will be.

Autofac支持自动注册,但我们认为使用另一个DI 容器来使用约定配置示例电子商务应用程序会更有趣。因为我们喜欢将示例限制在本书中讨论的DI 容器,并且因为 Microsoft.Extensions.DependencyInjection 没有任何自动注册功能,所以我们将使用简单注入器来说明这个概念。

Autofac supports Auto-Registration, but we thought it would be more interesting to use yet another DI Container to configure the sample e-commerce application using conventions. Because we like to restrain the examples to the DI Containers discussed in this book, and because Microsoft.Extensions.DependencyInjection doesn’t have any Auto-Registration facilities, we’ll use Simple Injector to illustrate this concept.

回顾清单 12.9,您可能会同意各种数据访问组件的注册是重复的。我们可以围绕它们表达某种约定吗?清单 12.9中的所有五种具体存储库类型都有一些共同特征:

Looking back at listing 12.9, you’ll likely agree that the registrations of the various data access components are repetitive. Can we express some sort of convention around them? All five concrete Repository types of listing 12.9 share some characteristics:

  • 它们都在同一个程序集中定义。
  • They’re all defined in the same assembly.
  • 每个具体类都有一个以Repository结尾的名称。
  • Each concrete class has a name that ends with Repository.
  • 每个都实现一个接口。
  • Each implements a single interface.

似乎适当的约定会通过扫描有问题的程序集并注册所有符合约定的类来表达这些相似性。即使 Simple Injector 确实支持Auto-Registration,它的Auto-Registration API 侧重于注册共享相同接口的类型组。它的 API 本身不允许您表达这种约定,因为没有单一的接口来描述这组存储库。

It seems that an appropriate convention would express these similarities by scanning the assembly in question and registering all classes that match the convention. Even though Simple Injector does support Auto-Registration, its Auto-Registration API focuses around the registration of groups of types that share the same interface. Its API, by itself, doesn’t allow you to express this convention, because there’s no single interface that describes this group of repositories.

起初,这种遗漏可能看起来相当尴尬,但在 .NET 的反射 API 之上定义自定义 LINQ 查询通常很容易编写,提供更多的灵活性,并且使您不必学习另一个 API — 假设您熟悉 LINQ和 .NET 的反射 API。以下清单显示了使用 LINQ 查询的此类约定。

At first, this omission might seem rather awkward, but defining a custom LINQ query on top of .NET’s reflection API is typically easy to write, provides more flexibility, and prevents you from having to learn another API — assuming you’re familiar with LINQ and .NET’s reflection API. The following listing shows such a convention using a LINQ query.

清单 12.10 使用 Simple Injector 扫描存储库的约定

Listing 12.10 Convention for scanning repositories using Simple Injector

var assembly =    ①  
    typeof(SqlProductRepository).Assembly;    ①  

var repositoryTypes =    ②  
    from type in assembly.GetTypes()    ②  
    where !type.Abstract    ②  
    where type.Name.EndsWith("Repository")    ②  
    select type;    ②  

foreach (Type type in repositoryTypes)    ③  
{    ③  
    container.Register(    ③  
        type.GetInterfaces().Single(), type);  ③  
}    ③  

在迭代期间通过过滤器的每个类都where应该针对它们的接口进行注册。例如,因为SqlProductRepository的接口是,它最终会作为从到的映射。IProductRepositoryIProductRepositorySqlProductRepository

Each of the classes that make it through the where filters during iteration should be registered against their interface. For example, because SqlProductRepository’s interface is an IProductRepository, it’ll end up as a mapping from IProductRepository to SqlProductRepository.

此特定约定扫描包含数据访问组件的程序集。您可以通过多种方式获得对该程序集的引用,但最简单的方法是选择一个代表性类型,例如SqlProductRepository,并从中获取程序集,如清单 12.10所示。您也可以选择不同的类或按名称找到程序集。

This particular convention scans the assembly that contains the data access components. You could get a reference to that assembly in many ways, but the easiest way is to pick a representative type, such as SqlProductRepository, and get the assembly from that, as shown in listing 12.10. You could also have chosen a different class or found the assembly by name.

将此约定与清单 12.9中的四个注册进行比较,您可能认为此约定的好处看起来可以忽略不计。事实上,由于当前示例中只有四个数据访问组件,因此代码语句的数量随着约定而增加。但是这个约定的规模要好得多。一旦你编写它,它就可以处理数百个组件而无需任何额外的工作。

Comparing this convention against the four registrations in listing 12.9, you may think that the benefits of this convention look negligible. Indeed, because there are only four data access components in the current example, the amount of code statements has increased with the convention. But this convention scales much better. Once you write it, it handles hundreds of components without any additional effort.

您还可以使用约定处理清单 12.6 和 12.8 中的其他映射,但这样做没有多大价值。例如,您可以使用此约定注册所有服务:

You can also address the other mappings from listings 12.6 and 12.8 with conventions, but there wouldn’t be much value in doing so. As an example, you can register all services with this convention:

var assembly = typeof(ProductService).Assembly;

var serviceTypes =
    from type in assembly.GetTypes()
    where !type.Abstract
    where type.Name.EndsWith("Service")
    select type;

foreach (Type type in serviceTypes)
{
    container.Register(type.GetInterfaces().Single(), type);
}

此约定扫描已识别的程序集以查找名称以Service结尾的所有具体类,并针对其实现的接口注册每种类型。这有效地ProductService针对IProductService接口进行了注册,但是由于您目前没有任何其他匹配项符合此约定,因此没有什么收获。只有当添加了更多服务时,如清单 12.9所示,制定约定才开始有意义。

This convention scans the identified assembly for all concrete classes where the name ends with Service and registers each type against the interface it implements. This effectively registers ProductService against the IProductService interface, but because you currently don’t have any other matches for this convention, nothing much is gained. It’s only when more services are added, as indicated in listing 12.9, that it starts to make sense to formulate a convention.

使用 LINQ 手动定义约定对于所有派生自它们自己的接口的类型可能有意义,正如您之前在存储库中看到的那样。但是当你开始注册基于泛型接口的类型时,正如我们在 10.3.3 节中广泛讨论的那样,这种策略很快就会失效——通过反射查询泛型类型通常不是一件令人愉快的事情。6个 

Defining conventions by hand with the use of LINQ might make sense for types all deriving from their own interface, as you’ve seen previously with the repositories. But when you start to register types that are based on a generic interface, as we extensively discussed in section 10.3.3, this strategy starts to break down rather quickly — querying generic types through reflection is typically not a pleasant thing to do.6 

这就是为什么 Simple Injector 的自动注册API 是围绕基于通用抽象的类型注册构建的,例如ICommandService<TCommand>接口来自清单 10.12。Simple Injector 允许在ICommandService<TCommand>一行代码中完成所有实现的注册。

That’s why Simple Injector’s Auto-Registration API is built around the registration of types based on a generic Abstraction, such as the ICommandService<TCommand> interface from listing 10.12. Simple Injector allows the registration of all ICommandService<TCommand> implementations to be done in a single line of code.

清单 12.11 基于通用抽象的自动注册实现

Listing 12.11 Auto-Registering implementations based on a generic Abstraction

Assembly assembly = typeof(AdjustInventoryService).Assembly;

container.Register(typeof(ICommandService<>), assembly);

通过向其Register重载之一提供程序集列表,Simple Injector 遍历这些程序集以查找实现的任何非通用的具体类型ICommandService<TCommand>,同时通过其特定的注册每个类型ICommandService<TCommand> 接口。TCommand这具有用实际类型填充的泛型类型参数。

By supplying a list of assemblies to one of its Register overloads, Simple Injector iterates through these assemblies to find any non-generic, concrete types that implement ICommandService<TCommand>, while registering each type by its specific ICommandService<TCommand> interface. This has the generic type argument TCommand filled in with an actual type.

在具有四个ICommandService<TCommand>实现的应用程序中,先前的 API 调用将等效于以下配置即代码清单。

In an application with four ICommandService<TCommand> implementations, the previous API call would be equivalent to the following Configuration as Code listing.

气味.tif

清单 12.12使用配置即代码 注册实现

Listing 12.12 Registering implementations using Configuration as Code

container.Register(typeof(ICommandService<AdjustInventory>),
    typeof(AdjustInventoryService));
container.Register(typeof(ICommandService<UpdateProductReviewTotals>),
    typeof(UpdateProductReviewTotalsService));
container.Register(typeof(ICommandService<UpdateHasDiscountsApplied>),
    typeof(UpdateHasDiscountsAppliedService));
container.Register(typeof(ICommandService<UpdateHasTierPricesProperty>),
    typeof(UpdateHasTierPricesPropertyService));

然而,迭代程序集列表以找到合适的类型并不是您可以使用 Simple Injector 的自动注册API实现的唯一目标。另一个强大的特性是通用装饰器的注册,就像您在清单 10.15、10.16 和 10.19 中看到的那样。与清单 10.21 中那样手动组合装饰器的层次结构不同,Simple Injector 允许使用其RegisterDecorator方法重载来应用装饰器。

Iterating a list of assemblies to find appropriate types, however, isn’t the only thing you can achieve with Simple Injector’s Auto-Registration API. Another powerful feature is the registration of generic Decorators, like the ones you saw in listings 10.15, 10.16, and 10.19. Instead of manually composing the hierarchy of Decorators, as you did in listing 10.21, Simple Injector allows Decorators to be applied using its RegisterDecorator method overloads.

清单 12.13使用自动 注册注册通用装饰器

Listing 12.13 Registering generic Decorators using Auto-Registration

container.RegisterDecorator(    ①  
    typeof(ICommandService<>),    ①  
    typeof(AuditingCommandServiceDecorator<>));    ①  
    ①  
container.RegisterDecorator(    ①  
    typeof(ICommandService<>),    ①  
    typeof(TransactionCommandServiceDecorator<>));    ①  
    ①  
container.RegisterDecorator(    ①  
    typeof(ICommandService<>),    ①  
    typeof(SecureCommandServiceDecorator<>));    ①  

Simple Injector 按注册顺序应用装饰器,这意味着,对于清单 12.13,审计装饰器使用事务装饰器包装,事务装饰器使用安全装饰器包装,导致对象图与所示对象图相同在清单 10.21 中。

Simple Injector applies Decorators in order of registration, which means that, in respect to listing 12.13, the auditing Decorator is wrapped using the transaction Decorator, and the transaction Decorator is wrapped with the security Decorator, resulting in an object graph identical to the one shown in listing 10.21.

开放泛型类型的注册可以看作是自动注册的一种形式,因为单个方法调用可以导致装饰器被RegisterDecorator适用于许多注册。7  如果没有这种形式的通用装饰器类的自动注册,您将被迫为每个封闭的ICommandService<TCommand>实现分别注册每个装饰器的每个封闭版本,如下面的清单所示。

Registration of open-generic types can be seen as a form of Auto-Registration because a single method call to RegisterDecorator can result in a Decorator being applied to many registrations.7  Without this form of Auto-Registration for generic Decorator classes, you’d be forced to register each closed version of each Decorator for each closed ICommandService<TCommand> implementation individually, as the following listing shows.

坏.tif

清单 12.14使用配置作为代码 注册通用装饰器

Listing 12.14 Registering generic Decorators using Configuration as Code

container.RegisterDecorator(
    typeof(ICommandService<AdjustInventory>),
    typeof(AuditingCommandServiceDecorator<AdjustInventory>));
container.RegisterDecorator(
    typeof(ICommandService<AdjustInventory>),
    typeof(TransactionCommandServiceDecorator<AdjustInventory>));
container.RegisterDecorator(
    typeof(ICommandService<AdjustInventory>),
    typeof(SecureCommandServiceDecorator<AdjustInventory>));

container.RegisterDecorator(
    typeof(ICommandService<UpdateProductReviewTotals>),
    typeof(AuditingCommandServiceDecorator<UpdateProductReviewTotals>));
container.RegisterDecorator(
    typeof(ICommandService<UpdateProductReviewTotals>),
    typeof(TransactionCommandServiceDecorator<UpdateProductReviewTotals>));
container.RegisterDecorator(
    typeof(ICommandService<UpdateProductReviewTotals>),
    typeof(SecureCommandServiceDecorator<UpdateProductReviewTotals>));

container.RegisterDecorator(
    typeof(ICommandService<UpdateHasDiscountsApplied>),
    typeof(AuditingCommandServiceDecorator<UpdateHasDiscountsApplied>));
container.RegisterDecorator(
    typeof(ICommandService<UpdateHasDiscountsApplied>),
    typeof(TransactionCommandServiceDecorator<UpdateHasDiscountsApplied>));
container.RegisterDecorator(
    typeof(ICommandService<UpdateHasDiscountsApplied>),
    typeof(SecureCommandServiceDecorator<UpdateHasDiscountsApplied>));

...   ① 

此清单中的代码繁琐且容易出错。此外,它会导致Composition Root呈指数增长。

The code in this listing is cumbersome and error prone. Additionally, it would cause an exponential growth of the Composition Root.

在遵循SOLID原则的系统中,您创建了许多小而集中的类,但现有类不太可能更改,从而提高了可维护性。自动注册可防止组合根不断更新。这是一种强大的技术,有可能使DI 容器不可见。一旦适当的约定就位,您可能只需要在极少数情况下修改容器配置。

In a system that adheres to the SOLID principles, you create many small and focused classes, but existing classes are less likely to change, increasing maintainability. Auto-Registration prevents the Composition Root from constantly being updated. It’s a powerful technique that has the potential to make the DI Container invisible. Once appropriate conventions are in place, you may have to modify the container configuration only on rare occasions.

12.2.4 混合和匹配配置方法

12.2.4 Mixing and matching configuration approaches

到目前为止,您已经看到了三种不同的配置DI 容器的方法:

So far, you’ve seen three different approaches to configuring a DI Container:

  • 配置文件
  • Configuration files
  • 配置即代码
  • Configuration as Code
  • 自动注册
  • Auto-Registration

这些都不是相互排斥的。您可以选择将Auto-Registration与特定的抽象到具体类型的映射混合使用,甚至可以混合使用所有三种方法来获得一些Auto-Registration、一些Configuration as Code以及配置文件中的一些配置以用于后期绑定目的。

None of these are mutually exclusive. You can choose to mix Auto-Registration with specific mappings of abstract-to-concrete types, and even mix all three approaches to have some Auto-Registration, some Configuration as Code, and some of the configuration in configuration files for late binding purposes.

根据经验,您应该更喜欢自动注册作为起点,辅之以配置即代码来处理更多特殊情况。您应该为需要能够在不重新编译应用程序的情况下改变实现的情况保留配置文件——这种情况比您想象的要少。

As a rule of thumb, you should prefer Auto-Registration as a starting point, complemented by Configuration as Code to handle more special cases. You should reserve configuration files for cases where you need to be able to vary an implementation without recompiling the application — which is rarer than you may think.

现在我们已经介绍了如何配置DI 容器以及如何用一个解析对象图,您应该对如何使用它们有一个很好的了解。使用DI 容器是一回事,但了解何时使用它又是另一回事。

Now that we’ve covered how to configure a DI Container and how to resolve object graphs with one, you should have a good idea about how to use them. Using a DI Container is one thing, but understanding when to use one is another.

12.3 何时使用DI 容器

12.3 When to use a DI Container

在本书的前几部分中,我们仅使用纯 DI作为我们的对象组合方法. 这不仅仅是为了教育目的。可以单独使用Pure DI构建完整的应用程序。

In the previous parts of this book, we solely used Pure DI as our method of Object Composition. This wasn’t just for educational purposes. Complete applications can be built using Pure DI alone.

在 12.2 节中,我们讨论了DI 容器的不同配置方法以及自动注册的使用如何提高Composition Root的可维护性。但是与纯 DI相比,使用DI 容器会带来额外的成本和缺点。大多数(如果不是全部)DI 容器都是开源的,因此它们在货币意义上是免费的。但是因为开发人员的工时通常是软件开发中最昂贵的部分,所以任何增加开发和维护软件所需时间的事情都是成本,这就是我们将在这里讨论的内容。

In section 12.2, we talked about the different configuration methods of DI Containers and how the use of Auto-Registration can increase maintainability of your Composition Root. But the use of DI Containers comes with additional costs and disadvantages over Pure DI. Most, if not all, DI Containers are open source, so they’re free in a monetary sense. But because developer hours are typically the most expensive part of software development, anything that increases the time it takes to develop and maintain software is a cost, which is what we’ll talk about here.

在本节中,我们将比较优缺点,以便您可以就何时使用DI 容器以及何时坚持使用Pure DI做出明智的决定。让我们从使用DI 容器等库时经常被忽视的一个方面开始,即它们会带来成本和风险。

In this section, we’ll compare the advantages and disadvantages, so you can make an educated decision about when to use a DI Container and when to stick to Pure DI. Let’s start with an often overlooked aspect of using libraries such as DI Containers, which is that they introduce costs and risks.

12.3.1 使用第三方库涉及成本和风险

12.3.1 Using third-party libraries involves costs and risks

当图书馆在货币意义上是免费的时,我们开发人员往往会忽略使用它所涉及的其他成本。DI 容器可能被视为稳定依赖项(第 1.3.1 节),因此从 DI的角度来看,使用它不是问题。但还有其他问题需要考虑。与任何第三方库一样,使用DI 容器会带来成本和风险。

When a library is free in a monetary sense, we developers often tend to ignore the other costs involved in using it. A DI Container might be considered a Stable Dependency (section 1.3.1), so from a DI perspective, using one isn’t an issue. But there are other concerns to consider. As with any third-party library, using a DI Container comes with costs and risks.

任何图书馆最明显的成本是它的学习曲线——学习使用新图书馆需要时间。您必须了解它的 API、它的行为、它的怪癖和它的局限性。当您与一组开发人员在一起时,他们中的大多数人都必须了解如何以某种方式使用该库。只有一个知道如何使用该工具的开发人员可能会在短期内节省成本,但这种做法本身就是对项目连续性的一种负担。8个 

The most obvious cost of any library is its learning curve — it takes time to learn to use a new library. You have to learn its API, its behavior, its quirks, and its limitations. When you’re with a team of developers, most of them will have to understand how to work with that library in one way or another. Having just one developer that knows how to work with the tool might save costs in the short run, but such a practice is in itself a liability to the continuity of your project.8 

图书馆的行为、怪癖和限制可能并不完全符合您的需要。一个库可能会倾向于与你的软件所围绕的模型不同的模型。9  这通常是您在学习使用它时才会发现的东西。当您将它应用到您的代码库时,您可能会发现您需要实施各种变通办法。这会导致大量剃掉牦牛毛。

A library’s behavior, quirks, and limitations might not exactly suit your needs. A library might be opinionated towards a different model than the one your software is built around.9  This is typically something you only find out while you’re learning to use it. As you apply it to your code base, you may find that you need to implement various workarounds. This can result in much yak shaving.

因此,很难估计使用新库将为项目节省多少资金,因为学习成本通常难以实际估计。花在学习第三方库 API 上的累积时间不是构建应用程序本身所花费的时间,因此代表了实际成本。

It is, therefore, hard to estimate how much money the use of a new library will save the project because of the learning costs that are often hard to realistically estimate. The accumulated time spent on learning the API of a third-party library is time not spent building the application itself, and therefore represents a real cost.

除了学习使用图书馆工作的直接成本外,依赖这样的图书馆还存在风险。一个风险是开发人员停止维护和支持您正在使用的库。10  当发生此类事件时,它会给项目带来额外成本,因为它会迫使您更换库。在那种情况下,您将再次支付前面讨论的学习成本以及再次迁移和测试应用程序的额外成本。

Besides the direct cost of learning to work with a library, there are risks involved in taking a dependency on such a library. One risk is that the developers stop maintaining and supporting a library you’re using.10  When such an event occurs, it introduces extra costs to the project because it can force you to switch libraries. In that case, you’re paying the previously discussed learning costs all over again with the additional costs of migrating and testing the application again.

这听起来像是反对使用外部库的论点,但事实并非如此。如果没有外部库,您将无法提高工作效率,因为您必须重新发明轮子。如果不使用外部库意味着自己构建这样的库,那么您的情况往往会更糟。(而且我们开发人员往往会低估编写、测试和维护这样一款软件所需的时间。)

This all sounds like an argument against using external libraries, but that isn’t the case. You wouldn’t be productive without external libraries, because you’d have to reinvent the wheel. If not using an external library means building such a library yourself, you’ll often be worse off. (And we developers tend to underestimate the time it takes to write, test, and maintain such a piece of software.)

然而,使用DI Containers时,您的情况有所不同。这是因为使用外部DI 容器库的替代方法不是构建您自己的库,而是应用Pure DI

With DI Containers, however, you’re in a somewhat different situation. That’s because the alternative to using an external DI Container library isn’t to build your own, but to apply Pure DI.

正如您在 4.1 节中了解到的,与DI 容器的交互应仅限于Composition Root。这已经降低了必须更换时的风险。但即使在那种情况下,替换DI 容器并熟悉新的 API 和设计理念也可能是一项耗时的工作。

As you learned in section 4.1, interaction with the DI Container should be limited to the Composition Root. This already reduces the risk when it must be replaced. But even in that case, it can be a time-consuming endeavor to replace the DI Container and become familiar with a new API and design philosophy.

Pure DI的主要优点是它易于学习。您不必学习任何DI 容器的 API ,尽管个别类仍在使用 DI,但一旦您找到Composition Root,就会清楚发生了什么以及对象图是如何构建的。尽管较新的 IDE 减少了这个问题,但团队中的新开发人员可能很难了解构造的对象图并在使用DI 容器时找到类的依赖项的实现。

The major advantage of Pure DI is that it’s easy to learn. You don’t have to learn the API of any DI Container and, although individual classes still use DI, once you find the Composition Root, it’ll be evident what’s going on and how object graphs are constructed. Although newer IDEs make this less of a problem, it can be difficult for a new developer on a team to get a sense of the constructed object graph and to find the implementation for a class’s Dependency when a DI Container is used.

使用Pure DI,这不是一个问题,因为对象图的构造是硬编码在Composition Root中的。除了更容易学习之外,Pure DI还为您提供更短的反馈周期,以防您的对象组合出现错误。让我们接下来看看。

With Pure DI, this is less of a problem, because object graph construction is hard coded in the Composition Root. Besides being easier to learn, Pure DI gives you a shorter feedback cycle in case there’s an error in your composition of objects. Let’s look at that next.

12.3.2 纯 DI反馈周期更短

12.3.2 Pure DI gives a shorter feedback cycle

DI 容器技术,例如Auto-WiringAuto-Registration,依赖于反射的使用。这意味着,在运行时,DI 容器将使用反射分析构造函数参数,甚至通过完整的程序集进行查询,以根据约定查找类型,从而组成完整的对象图。因此,只有在解析对象图时才会在运行时检测到配置错误。与Pure DI相比,DI Container承担了编译器对代码校验的作用。

DI Container techniques, such as Auto-Wiring and Auto-Registration, depend on the use of reflection. This means that, at runtime, the DI Container will analyze constructor arguments using reflection or even query through complete assemblies to find types based on conventions in order to compose complete object graphs. Consequently, configuration errors are only detected at runtime when an object graph is resolved. Compared to Pure DI, the DI Container assumes the compiler’s role of code verification.

Composition Root的结构良好时,SingletonsScoped实例的创建是分开的(例如,参见清单 8.10 和 8.13),它允许编译器检测Captive Dependencies,如 8.4.1 节所述。

When a Composition Root is well structured so that the creation of Singletons and Scoped instances are separated (see listings 8.10 and 8.13, for instance), it allows the compiler to detect Captive Dependencies, as discussed in section 8.4.1.

正如我们在 3.2.2 节中讨论的那样,由于强类型,Pure DI还具有让您更清楚地了解应用程序对象图结构的优势。这是您在开始使用DI 容器时会立即失去的东西。

As we discussed in section 3.2.2, because of strong typing, Pure DI also has the advantage of giving you a clearer picture of the structure of the application’s object graphs. This is something that you’ll lose immediately when you start using a DI Container.

但强类型是双向的,因为正如我们在 12.1.3 节中讨论的那样,这也意味着每次重构构造函数时,都会破坏Composition Root。如果您在应用程序之间共享一个库(域模型、实用程序、数据访问组件等),您可能有多个组合根要维护。这有多大负担取决于您重构构造函数的频率,但我们已经看到每天发生几次这种情况的项目。由于多个开发人员在同一个项目上工作,这很容易导致合并冲突,这会花费时间来修复。

But strong typing cuts both ways because, as we discussed in section 12.1.3, it also means that every time you refactor a constructor, you’ll break the Composition Root. If you’re sharing a library (domain model, utility, data access component, and so on) between applications, you may have more than one Composition Root to maintain. How much of a burden this is depends on how often you refactor constructors, but we’ve seen projects where this happens several times each day. With multiple developers working on a single project, this can easily lead to merge conflicts, which cost time to fix.

虽然编译器在使用Pure DI时会给出快速反馈,但它能做的验证量是有限的。由于构造函数的更改以及某种程度上的Captive Dependencies ,它将能够报告缺少的Dependencies,但除其他外,它将无法检测到以下内容:

Although the compiler will give rapid feedback when using Pure DI, the amount of validations it can do is limited. It’ll be able to report missing Dependencies due to changes to constructors and to some extent Captive Dependencies, but, among other things, it will fail to detect the following:

  • 由于从构造函数体内抛出异常而导致构造函数调用失败(例如,失败的 Guard 子句)
  • Failing constructor invocations due to exceptions thrown from within the constructor’s body (for example, failing Guard Clauses)
  • 一次性组件超出范围时是否进行处理
  • Whether disposable components are disposed of when they go out of scope
  • 当应该是SingletonScoped的类再次(意外地)在Composition Root的不同部分创建时,可能具有不同的生活方式12 
  • When classes that are supposed to be Singleton or Scoped are again (accidentally) created in a different part of the Composition Root, possibly with a different lifestyle12 

使用Pure DI时,Composition Root的大小会随着应用程序的大小线性增长。当一个应用程序很小的时候,它的Composition Root也会很小。这使得它的Composition Root干净且易于管理,而且之前列出的缺陷也很容易被发现。但是当Composition Root增长时,就更容易错过这样的缺陷。

When using Pure DI, the size of the Composition Root grows linearly with the size of the application. When an application is small, its Composition Root will also be small. This makes its Composition Root clean and manageable, and previously listed defects will be easy to spot. But when the Composition Root grows, it becomes easier to miss such defects.

这是使用DI 容器可以缓解的问题。大多数DI 容器会代表您自动检测一次性组件,并可能检测常见的陷阱,例如Captive Dependencies13 

This is something that the use of a DI Container can mitigate. Most DI Containers automatically detect a disposable component on your behalf and might detect common pitfalls, such as Captive Dependencies.13 

12.3.3 结论:何时使用DI 容器

12.3.3 The verdict: When to use a DI Container

如果您使用DI ContainerConfiguration as Code功能(如 12.2.2 节所述),使用容器的 API 显式注册每个组件,您将失去来自强类型的快速反馈。另一方面,维护负担也可能因为Auto-Wiring而下降。尽管如此,您仍然需要在引入每个新类时注册它,这是一个线性增长,并且您和您的团队必须学习该容器的特定 API。但即使您已经熟悉它的 API,仍然存在有一天不得不更换它的风险。你可能失去的比得到的更多。

If you use a DI Container’s Configuration as Code abilities (as discussed in section 12.2.2), explicitly registering each and every component using the container’s API, you lose the rapid feedback from strong typing. On the other hand, the maintenance burden is also likely to drop because of Auto-Wiring. Still, you’ll need to register each new class when you introduce it, which is a linear growth, and you and your team have to learn the specific API of that container. But even if you’re already familiar with its API, there’s still the risk of having to replace it someday. You might lose more than you gain.

最终,如果您可以以足够复杂的方式使用DI 容器,您可以使用它来定义一组使用自动注册的约定(如第 12.2.3 节中所述)。这些约定定义了您的代码应遵守的规则集,只要您遵守这些规则,事情就会正常进行。容器落在背景中,您很少需要触摸它。

Ultimately, if you can wield a DI Container in a sufficiently sophisticated way, you can use it to define a set of conventions using Auto-Registration (as discussed in section 12.2.3). These conventions define a rule set that your code should adhere to, and as long as you stick to those rules, things just work. The container drops to the background, and you rarely need to touch it.

自动注册需要时间来学习,并且是弱类型的,但是,如果做得好,它可以让您专注于增加价值的代码而不是基础设施。另一个优点是它创建了一个积极的反馈机制,迫使团队生成与约定一致的代码。图 12.6可视化了纯 DI和使用DI 容器之间的权衡。

Auto-Registration takes time to learn, and is weakly typed, but, if done right, it enables you to focus on code that adds value instead of infrastructure. An additional advantage is that it creates a positive feedback mechanism, forcing a team to produce code that’s consistent with the conventions. Figure 12.6 visualizes the trade-off between Pure DI and using a DI Container.

正如我们在 12.2.4 节中所述,可用的方法都不是相互排斥的。尽管您可能会发现单个组合根包含所有配置样式的混合,但组合根应该侧重于纯 DI,可能有一些后期绑定类型,或者围绕自动注册,可选地,数量有限配置为代码纯 DI和配置文件。专注于配置即代码组合根是没有意义的,因此应该避免。

As we stated in section 12.2.4, none of the available approaches are mutually exclusive. Although you might find a single Composition Root to contain a mix of all configuration styles, a Composition Root should either be focused around Pure DI with, perhaps, a few late-bound types, or around Auto-Registration with, optionally, a limited amount of Configuration as Code, Pure DI, and configuration files. A Composition Root that focuses around Configuration as Code is pointless and should therefore be avoided.

12-06.eps

图 12.6 纯 DI 之所以有价值,是因为它很简单,尽管DI 容器可能是有价值的,也可能是毫无意义的,这取决于它的使用方式。当它以足够复杂的方式使用时(使用自动注册),我们认为DI 容器可以提供最佳的价值/成本比。

Figure 12.6 Pure DI can be valuable because it’s simple, although a DI Container can be either valuable or pointless, depending on how it’s used. When it’s used in a sufficiently sophisticated way (using Auto-Registration), we consider a DI Container to offer the best value/cost ratio.

那么问题就变成了:什么时候应该选择Pure DI,什么时候应该使用Auto-Registration?不幸的是,我们无法就此给出任何硬性数字。这取决于项目的规模、您和您的团队使用DI 容器的经验以及风险的计算。

The question then becomes this: when should you choose Pure DI, and when should you use Auto-Registration? We, unfortunately, can’t give any hard numbers on this. It depends on the size of the project, the amount of experience you and your team have with a DI Container, and the calculation of risk.

不过,一般来说,您应该对较小的组合根使用纯 DI ,并在维护此类组合根成为问题时切换到自动注册。具有许多类的更大的应用程序可以通过多个约定捕获,可以从使用自动注册中受益。14 

In general, though, you should use Pure DI for Composition Roots that are small and switch to Auto-Registration when maintaining such a Composition Root becomes a problem. Bigger applications with many classes that can be captured by several conventions can benefit from using Auto-Registration.14 

我们不会告诉您的另一件事是选择哪个DI 容器。选择DI 容器涉及的不仅仅是技术评估。您还必须评估许可模型是否可以接受,您是否信任开发和维护DI 容器的人员或组织,它如何适合您组织的 IT 战略,等等。您对合适的DI 容器的搜索也不应该仅限于本书中列出的容器。例如,许多优秀的 .NET 平台的DI 容器可供选择。

The other thing we won’t tell you is which DI Container to choose. Selecting a DI Container involves more than technical evaluation. You must also evaluate whether the licensing model is acceptable, whether you trust the people or organization that develops and maintains the DI Container, how it fits into your organization’s IT strategy, and so on. Your search for the right DI Container also shouldn’t be limited to the containers listed in this book. For example, many excellent DI Containers for the .NET platform are available to choose from.

如果正确使用DI 容器,它可能是一个有用的工具。最重要的是要了解 DI 的使用决不依赖于DI Container的使用。一个应用程序可以由许多松散耦合的类和模块组成,而这些模块中没有一个对容器一无所知。确保应用程序代码不知道任何DI 容器的最有效方法是将其使用限制在Composition Root中。这可以防止您无意中应用服务定位器反模式,因为它将容器限制在代码的一个小的、孤立的区域。

A DI Container can be a helpful tool if you use it correctly. The most important thing to understand is that the use of DI in no way depends on the use of a DI Container. An application can be made from many loosely coupled classes and modules, and none of these modules knows anything about a container. The most effective way to make sure that application code is unaware of any DI Container is by limiting its use to the Composition Root. This prevents you from inadvertently applying the Service Locator anti-pattern, because it constrains the container to a small, isolated area of the code.

以这种方式使用,DI 容器成为一个引擎,负责处理部分应用程序的基础架构。它根据其配置组成对象图。如果您使用 Convention over Configuration,这将特别有用。如果实施得当,它可以负责组合对象图,您可以集中精力实施新功能。容器将自动发现遵循既定约定的新类,并将它们提供给消费者。本书的最后三章涵盖了 Autofac(第 13 章)、Simple Injector(第 14 章)和 Microsoft.Extensions.DependencyInjection(第 15 章)。

Used in this way, a DI Container becomes an engine that takes care of part of the application’s infrastructure. It composes object graphs based on its configuration. This can be particularly beneficial if you employ Convention over Configuration. If suitably implemented, it can take care of composing object graphs, and you can concentrate your efforts on implementing new features. The container will automatically discover new classes that follow the established conventions and make them available to consumers. The final three chapters of this book cover Autofac (chapter 13), Simple Injector (chapter 14), and Microsoft.Extensions.DependencyInjection (chapter 15).

概括

Summary

  • DI 容器是提供 DI 功能的库。它是一个解析和管理对象图的引擎。
  • A DI Container is a library that provides DI functionality. It’s an engine that resolves and manages object graphs.
  • DI 决不依赖于DI Container的使用。DI 容器是一个有用但可选的工具。
  • DI in no way hinges on the use of a DI Container. A DI Container is a useful, but optional, tool.
  • 自动装配是通过使用编译器和公共语言运行时 (CLR) 提供的类型信息,从抽象类型和具体类型之间的映射自动组成对象图的能力。
  • Auto-Wiring is the ability to automatically compose an object graph from maps between Abstractions and concrete types by making use of the type information as supplied by the compiler and the Common Language Runtime (CLR).
  • 构造函数注入静态地公布类的依赖关系要求,DI 容器使用该信息自动连接复杂的对象图。
  • Constructor Injection statically advertises the Dependency requirements of a class, and DI Containers use that information to Auto-Wire complex object graphs.
  • Auto-Wiring使Composition Root更能适应变化。
  • Auto-Wiring makes a Composition Root more resilient to change.
  • 当您开始使用DI 容器时,您不需要完全放弃手工连接对象图。当这样更方便时,您可以在部分配置中使用手动接线。
  • When you start using a DI Container, you’re not required to abandon hand wiring object graphs altogether. You can use hand wiring in parts of your configuration when this is more convenient.
  • 使用DI Container时,三种配置样式是配置文件、Configuration as CodeAuto-Registration
  • When using a DI Container, the three configuration styles are configuration files, Configuration as Code, and Auto-Registration.
  • 配置文件与配置、代码自动注册一样,都是Composition Root的一部分。因此,使用配置文件不会使您的Composition Root变小,它只会移动它。
  • Configuration files are as much a part of your Composition Root as Configuration as Code and Auto-Registration. Using configuration files, therefore, doesn’t make your Composition Root smaller, it just moves it.
  • 随着应用程序的大小和复杂性的增加,您的配置文件也会增加。配置文件往往变得脆弱且对错误不透明,因此仅在需要后期绑定时才使用此方法。
  • As your application grows in size and complexity, so will your configuration file. Configuration files tend to become brittle and opaque to errors, so only use this approach when you need late binding.
  • 不要因为不支持处理配置文件而影响您选择DI 容器的选择。可以通过几个简单的语句从配置文件中加载类型。
  • Don’t let the absence of support for handling configuration files influence your choice for picking a DI Container. Types can be loaded from configuration files in a few simple statements.
  • 配置即代码允许将容器的配置存储为源代码。抽象和特定实现之间的每个映射都在代码中明确和直接地表达。除非您需要后期绑定,否则此方法优于配置文件。
  • Configuration as Code allows the container’s configuration to be stored as source code. Each mapping between an Abstraction and a particular implementation is expressed explicitly and directly in code. This method is preferred over configuration files unless you need late binding.
  • Convention over Configuration 是将约定应用于您的代码,以方便注册。
  • Convention over Configuration is the application of conventions to your code to facilitate easier registration.
  • 自动注册是一种通过扫描一个或多个程序集以实现所需抽象来自动在容器中注册组件的能力,这是一种约定优于配置的形式。
  • Auto-Registration is the ability to automatically register components in a container by scanning one or more assemblies for implementations of desired Abstractions, which is a form of Convention over Configuration.
  • Auto-Registration有助于避免不断更新Composition Root,因此比Configuration as Code更受欢迎。
  • Auto-Registration helps avoid constantly updating the Composition Root and is, therefore, preferred over Configuration as Code.
  • 使用DI 容器等外部库会产生成本和风险;例如,学习新 API 的成本和库被遗弃的风险。
  • Using external libraries such as DI Containers incurs costs and risks; for example, the cost of learning a new API and the risk of the library being abandoned.
  • 避免构建自己的DI 容器。要么使用现有的、经过良好测试且免费提供的DI 容器之一,要么练习Pure DI。创建和维护这样一个库需要付出很多努力,而这些努力并没有花在产生商业价值上。
  • Avoid building your own DI Container. Either use one of the existing, well-tested, and freely available DI Containers, or practice Pure DI. Creating and maintaining such a library takes a lot of effort, which is effort not spent producing business value.
  • Pure DI的一大优点是它是强类型的。这允许编译器提供有关正确性的反馈,这是您可以获得的最快反馈。
  • The big advantage of Pure DI is that it’s strongly typed. This allows the compiler to provide feedback about correctness, which is the fastest feedback that you can get.
  • 您应该对较小的组合根使用纯 DI ,并在维护此类组合根成为问题时切换到自动注册。具有许多类的更大的应用程序可以通过多个约定捕获,可以从使用自动注册中获益匪浅。
  • You should use Pure DI for Composition Roots that are small and switch to Auto-Registration whenever maintaining such Composition Roots becomes a problem. Bigger applications with many classes that can be captured by several conventions can greatly benefit from using Auto-Registration.

13

Autofac DI 容器

13

The Autofac DI Container

在这一章当中

In this chapter

  • 使用 Autofac 的基本注册 API
  • Working with Autofac’s basic registration API
  • 管理组件生命周期
  • Managing component lifetime
  • 配置困难的 API
  • Configuring difficult APIs
  • 配置序列、装饰器和组合
  • Configuring sequences, Decorators, and Composites

在前面的章节中,我们讨论了一般适用于 DI 的模式和原则,但是,除了几个示例之外,我们还没有详细了解如何使用任何特定的DI 容器来应用它们。在本章中,您将看到这些总体模式如何映射到 Autofac。您需要熟悉前几章的材料才能从中充分受益。

In the previous chapters, we discussed patterns and principles that apply to DI in general, but, apart from a few examples, we’ve yet to take a detailed look at how to apply them using any particular DI Container. In this chapter, you’ll see how these overall patterns map to Autofac. You’ll need to be familiar with the material from the previous chapters to fully benefit from this.

Autofac 是一个相当全面的DI 容器,它提供了精心设计和一致的 API。它自 2007 年末以来一直存在,并且在撰写本文时是最受欢迎的容器之一。1个 

Autofac is a fairly comprehensive DI Container that offers a carefully designed and consistent API. It’s been around since late 2007 and is, at the time of writing, one of the most popular containers.1 

在本章中,我们将研究如何使用 Autofac 来应用第 1-3 部分中介绍的原则和模式。本章分为四节。您可以独立阅读每个部分,尽管第一部分是其他部分的先决条件,而第四部分依赖于第三部分介绍的一些方法和类。

In this chapter, we’ll examine how Autofac can be used to apply the principles and patterns presented in parts 1–3. This chapter is divided into four sections. You can read each section independently, though the first section is a prerequisite for the other sections, and the fourth section relies on some methods and classes introduced in the third section.

本章应该使您能够入门,并处理日常使用 Autofac 时可能出现的最常见问题。这不是对 Autofac 的完整处理;那将需要更多的章节或者本身可能是一整本书。如果您想了解有关 Autofac 的更多信息,最好从 Autofac 主页https://autofac.org开始。

This chapter should enable you to get started, as well as deal with the most common issues that can come up as you use Autofac on a daily basis. It’s not a complete treatment of Autofac; that would take several more chapters or perhaps a whole book in itself. If you want to know more about Autofac, the best place to start is at the Autofac home page at https://autofac.org.

13.1 介绍Autofac

13.1 Introducing Autofac

在本节中,您将了解从何处获得 Autofac、可以获得什么以及如何开始使用它。我们还将查看常见的配置选项。表 13.1提供了开始时可能需要的基本信息。

In this section, you’ll learn where to get Autofac, what you get, and how you start using it. We’ll also look at common configuration options. Table 13.1 provides fundamental information that you’re likely to need to get started.

表 13.1 Autofac 概览
回答
我从哪里得到它?在 Visual Studio 中,您可以通过 NuGet 获取它。包名称是Autofac。或者,可以从 GitHub 存储库 ( https://github.com/autofac/Autofac/releases ) 下载 NuGet 包。
支持哪些平台?.NET 4.5(没有 .NET Core SDK)和 .NET Standard 1.1(.NET Core 1.0、Mono 4.6、Xamarin.iOS 10.0、Xamarin.Mac 3.0、Xamarin.Android 7.0、UWP 10.0、Windows 8.0、Windows Phone 8.1) . 支持 .NET 2.0 和 Silverlight 的旧版本可通过 NuGet 历史获得。
它要多少钱?没有什么。它是开源的。
它是如何获得许可的?麻省理工学院许可证。
我在哪里可以得到帮助?您可以从与 Autofac 开发人员相关的公司获得商业支持。在https://autofac.readthedocs.io/en/latest/support.html阅读有关选项的更多信息。除了商业支持之外,Autofac 仍然是具有繁荣生态系统的开源软件,因此您也可能(但不保证)通过在https://stackoverflow.com上的 Stack Overflow 上发帖或使用官方论坛获得帮助https://groups.google.com/group/autofac
本章基于哪个版本?4.9.0-beta1

使用 Autofac 与使用我们将在后续章节中讨论的其他DI 容器没有什么不同。与 Simple Injector 和 Microsoft.Extensions.DependencyInjection 一样,使用过程分为两步,如图 13.1所示。首先,您配置一个ContainerBuilder,完成后,您可以使用它来构建一个容器来解析组件。

Using Autofac isn’t that different from using the other DI Containers that we’ll discuss in the following chapters. As with Simple Injector and Microsoft.Extensions.DependencyInjection, usage is a two-step process, as figure 13.1 illustrates. First, you configure a ContainerBuilder, and when you’re done with that, you use it to build a container to resolve components.

13-01.eps

图 13.1 使用 Autofac 的模式是先配置它,然后解析组件。

Figure 13.1 The pattern for using Autofac is to first configure it, and then resolve components.

完成本节后,您应该对 Autofac 的整体使用模式有一个良好的感觉,并且您应该能够在行为良好的场景中开始使用它——所有组件都遵循正确的 DI 模式,如构造函数注入。让我们从最简单的场景开始,看看如何使用 Autofac 容器解析对象。

When you’re done with this section, you should have a good feeling for the overall usage pattern of Autofac, and you should be able to start using it in well-behaved scenarios — where all components follow proper DI patterns like Constructor Injection. Let’s start with the simplest scenario and see how you can resolve objects using an Autofac container.

13.1.1 解析对象

13.1.1 Resolving objects

任何DI 容器的核心服务都是组合对象图。在本节中,我们将了解可让您使用 Autofac 组合对象图的 API。

The core service of any DI Container is to compose object graphs. In this section, we’ll look at the API that lets you compose object graphs with Autofac.

默认情况下,Autofac 要求您在解析之前注册所有相关组件。但是,此行为是可配置的。下面的清单显示了 Autofac 最简单的可能用途之一。

By default, Autofac requires you to register all relevant components before you can resolve them. This behavior, however, is configurable. The following listing shows one of the simplest possible uses of Autofac.

清单 13.1 Autofac 最简单的使用

Listing 13.1 Simplest possible use of Autofac

var builder = new ContainerBuilder();

builder.RegisterType<SauceBéarnaise>();

IContainer container = builder.Build();

ILifetimeScope scope = container.BeginLifetimeScope();

SauceBéarnaise sauce = scope.Resolve<SauceBéarnaise>();

如图13.1所示,您需要一个ContainerBuilder实例来配置组件。在这里,您注册了具体SauceBéarnaise类,builder这样当您要求它构建容器时,生成的容器就会使用SauceBéarnaise该类进行配置。这再次使您能够SauceBéarnaise从容器中解析类。

As figure 13.1 shows, you need a ContainerBuilder instance to configure components. Here, you register the concrete SauceBéarnaise class with builder so that when you ask it to build a container, the resulting container is configured with the SauceBéarnaise class. This again enables you to resolve the SauceBéarnaise class from the container.

但是,使用 Autofac,您永远不会从根容器本身解析,而是从生命周期范围解析。第 13.2.1 节详细介绍了生命周期范围以及为什么从根容器解析是一件坏事。

With Autofac, however, you never resolve from the root container itself, but from a lifetime scope. Section 13.2.1 goes into more detail about lifetime scope and why resolving from the root container is a bad thing.

如果您不注册SauceBéarnaise组件,尝试解析它会抛出以下消息:ComponentNotRegisteredException

If you don’t register the SauceBéarnaise component, attempting to resolve it throws a ComponentNotRegisteredException with the following message:

请求的服务“Ploeh.Samples.MenuModel.SauceBéarnaise”尚未注册。要避免此异常,请注册组件以提供服务,使用 IsRegistered() 检查服务注册,或使用 ResolveOptional() 方法解析可选依赖项。

The requested service "Ploeh.Samples.MenuModel.SauceBéarnaise" has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency.

Autofac 不仅可以使用无参数构造函数解析具体类型,还可以使用其他Dependencies自动连接类型。所有这些依赖项都需要注册。大多数情况下,您会希望针对接口进行编程,因为这会引入松散耦合。为了支持这一点,Autofac 允许您将抽象映射到具体类型。

Not only can Autofac resolve concrete types with parameterless constructors, it can also Auto-Wire a type with other Dependencies. All these Dependencies need to be registered. For the most part, you’ll want to program to interfaces, because this introduces loose coupling. To support this, Autofac lets you map Abstractions to concrete types.

将抽象映射到具体类型

Mapping Abstractions to concrete types

虽然您的应用程序的根类型通常由它们的具体类型解析,但松散耦合需要您将抽象映射到具体类型。基于此类映射创建实例是任何DI Container提供的核心服务,但您仍然必须定义映射。在此示例中,您将IIngredient接口映射到具体SauceBéarnaise类,这使您可以成功解析IIngredient

Whereas your application’s root types will typically be resolved by their concrete types, loose coupling requires you to map Abstractions to concrete types. Creating instances based on such maps is the core service offered by any DI Container, but you must still define the map. In this example, you map the IIngredient interface to the concrete SauceBéarnaise class, which allows you to successfully resolve IIngredient:

var builder = new ContainerBuilder();

builder.RegisterType<SauceBéarnaise>()
    .As<IIngredient>();    ①  

IContainer container = builder.Build();

ILifetimeScope scope = container.BeginLifetimeScope();

IIngredient sauce = scope.Resolve<IIngredient>();    ②  

As<T>方法允许将具体类型映射到特定的抽象。由于之前的As<IIngredient>()调用,SauceBéarnaise现在可以解析为IIngredient.

The As<T> method allows a concrete type to be mapped to a particular Abstraction. Because of the previous As<IIngredient>() call, SauceBéarnaise can now be resolved as IIngredient.

您使用该ContainerBuilder实例来注册类型和定义映射。该RegisterType方法允许您注册具体类型。

You use the ContainerBuilder instance to register types and define maps. The RegisterType method lets you register a concrete type.

正如您在清单 13.1中看到的,如果您只想注册SauceBéarnaise课程,您可以就此打住。您还可以继续该As方法来定义应如何注册具体类型。2个 

As you saw in listing 13.1, you can stop right there if you only want to register the SauceBéarnaise class. You can also continue with the As method to define how the concrete type should be registered.2 

在许多情况下,通用 API 就是您所需要的。尽管它不提供与某些其他DI 容器相同程度的类型安全性,但它仍然是配置容器的可读方式。不过,在某些情况下,您需要一种更弱类型的方式来解析服务。使用 Autofac,这也是可能的。

In many cases, the generic API is all you need. Although it doesn’t offer the same degree of type safety as some other DI Containers, it’s still a readable way to configure the container. Still, there are situations where you need a more weakly typed way to resolve services. With Autofac, this is also possible.

解决弱类型服务

Resolving weakly typed services

有时您不能使用通用 API,因为您在设计时不知道合适的类型。您只有一个Type实例,但您仍然希望获得该类型的实例。您在第 7.3 节中看到了一个示例,其中我们讨论了 ASP.NET Core MVC 的IControllerActivator. 相关方法是这样的:

Sometimes you can’t use a generic API, because you don’t know the appropriate type at design time. All you have is a Type instance, but you’d still like to get an instance of that type. You saw an example of that in section 7.3, where we discussed ASP.NET Core MVC’s IControllerActivator class. The relevant method is this:

object Create(ControllerContext context);

如前面清单 7.8 所示,ControllerContext捕获控制器的Type,您可以使用ControllerTypeInfo属性提取它ActionDescriptor财产的:

As shown previously in listing 7.8, the ControllerContext captures the controller’s Type, which you can extract using the ControllerTypeInfo property of the ActionDescriptor property:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();

因为你只有一个Type实例,你不能使用泛型Resolve<T>方法,但必须求助于弱类型的 API。Autofac 提供了Resolve方法的弱类型重载,使您可以Create像这样实现方法:

Because you only have a Type instance, you can’t use the generic Resolve<T> method, but must resort to a weakly typed API. Autofac offers a weakly typed overload of the Resolve method that lets you implement the Create method like this:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return scope.Resolve(controllerType);

的弱类型重载Resolve允许您传递controllerType变量直接到Autofac。通常,这意味着您必须将返回值转换为某种抽象,因为弱类型Resolve方法返回object。但是,在 的情况下IControllerActivator,这不是必需的,因为 ASP.NET Core MVC 不需要控制器来实现任何接口或基类。

The weakly typed overload of Resolve lets you pass the controllerType variable directly to Autofac. Typically, this means you have to cast the returned value to some Abstraction, because the weakly typed Resolve method returns object. In the case of IControllerActivator, however, this isn’t required, because ASP.NET Core MVC doesn’t require controllers to implement any interface or base class.

无论Resolve您使用哪种重载,Autofac 都保证它会返回所请求类型的实例,或者在存在无法满足的依赖项时抛出异常。当所有必需的依赖项都已正确配置后,Autofac 可以自动连接请求的类型。

No matter which overload of Resolve you use, Autofac guarantees that it’ll return an instance of the requested type or throw an exception if there are Dependencies that can’t be satisfied. When all required Dependencies have been properly configured, Autofac can Auto-Wire the requested type.

在前面的示例中,scope是 的一个实例Autofac.ILifetimeScope。为了能够解析请求的类型,所有松散耦合的依赖项必须已预先配置。配置 Autofac 的方法有很多种,下一节将介绍最常用的方法。

In the previous example, scope is an instance of Autofac.ILifetimeScope. To be able to resolve the requested type, all loosely coupled Dependencies must have been previously configured. There are many ways to configure Autofac, and the next section reviews the most common ones.

13.1.2 配置ContainerBuilder

13.1.2 Configuring the ContainerBuilder

正如我们在 12.2 节中讨论的那样,您可以通过几种概念上不同的方式配置DI 容器。图 12.5 查看了选项:配置文件、配置即代码自动注册图 13.2再次显示了这些选项。

As we discussed in section 12.2, you can configure a DI Container in several conceptually different ways. Figure 12.5 reviewed the options: configuration files, Configuration as Code, and Auto-Registration. Figure 13.2 shows these options again.

13-02.eps

图 13.2针对显式维度和绑定程度显示的配置DI 容器 的最常用方法

Figure 13.2 The most common ways to configure a DI Container shown against dimensions of explicitness and the degree of binding

核心配置 API 以代码为中心,同时支持Configuration as Code和基于约定的Auto-Registration。可以使用 Autofac.Configuration NuGet 包插入对配置文件的支持。Autofac 支持所有三种方法,并允许您将它们全部混合在同一个容器中。在本节中,您将看到如何使用这三种类型的配置源中的每一种。

The core configuration API is centered on code and supports both Configuration as Code and convention-based Auto-Registration. Support for configuration files can be plugged in using the Autofac.Configuration NuGet package. Autofac supports all three approaches and lets you mix them all within the same container. In this section, you’ll see how to use each of these three types of configuration sources.

配置ContainerBuilder使用配置作为代码

Configuring the ContainerBuilder using Configuration as Code

在 13.1 节中,您简要了解了 Autofac 的强类型配置 API。在这里,我们将更详细地研究它。

In section 13.1, you saw a brief glimpse of Autofac’s strongly typed configuration API. Here, we’ll examine it in greater detail.

Autofac 中的所有配置都使用该类公开的 API ContainerBuilder,尽管您使用的大多数方法都是扩展方法。最常用的方法之一是RegisterType方法你已经看到了:

All configurations in Autofac use the API exposed by the ContainerBuilder class, although most of the methods you use are extension methods. One of the most commonly used methods is the RegisterType method that you’ve already seen:

builder.RegisterType<SauceBéarnaise>().As<IIngredient>();

注册SauceBéarnaiseIIngredient隐藏具体类,因此您无法再SauceBéarnaise通过此注册进行解析。As但是您可以通过使用允许您指定具体类型映射到多个注册类型的方法的重载来轻松解决此问题:

Registering SauceBéarnaise as IIngredient hides the concrete class so that you can no longer resolve SauceBéarnaise with this registration. But you can easily fix this by using an overload of the As method that lets you specify that the concrete type maps to more than one registered type:

builder.RegisterType<SauceBéarnaise>().As<SauceBéarnaise, IIngredient>();

IIngredient您可以将类注册为它本身和它实现的接口,而不是仅将类注册为 。SauceBéarnaise这使容器能够解决对和的请求IIngredient。作为替代方案,您还可以链接调用该As方法:

Instead of registering the class only as IIngredient, you can register it as both itself and the interface it implements. This enables the container to resolve requests for both SauceBéarnaise and IIngredient. As an alternative, you can also chain calls to the As method:

builder.RegisterType<SauceBéarnaise>()
    .As<SauceBéarnaise>()
    .As<IIngredient>();

这将产生与上一个示例相同的结果。两种注册之间的区别只是风格问题。

This produces the same result as in the previous example. The difference between the two registrations is simply a matter of style.

该方法的三个泛型重载As允许您指定一种、两种或三种类型。如果您需要指定更多,还可以使用非泛型重载来指定任意数量的类型。

Three generic overloads of the As method let you specify one, two, or three types. If you need to specify more, there’s also a non-generic overload that you can use to specify as many types as you like.

在实际应用中,你总是有不止一个抽象要映射,所以你必须配置多个映射。这是通过多次调用来完成的RegisterType

In real applications, you always have more than one Abstraction to map, so you must configure multiple mappings. This is done with multiple calls to RegisterType:

builder.RegisterType<SauceBéarnaise>().As<IIngredient>();
builder.RegisterType<Course>().As<ICourse>();

此示例映射IIngredientSauceBéarnaiseICourseCourse没有类型重叠,所以应该很明显发生了什么。但是您也可以多次注册相同的抽象:

This example maps IIngredient to SauceBéarnaise, and ICourse to Course. There’s no overlap of types, so it should be pretty evident what’s going on. But you can also register the same Abstraction several times:

builder.RegisterType<SauceBéarnaise>().As<IIngredient>();
builder.RegisterType<Steak>().As<IIngredient>();

在这里,你注册IIngredient了两次。如果您解析IIngredient,您将获得 的一个实例Steak。最后一次注册获胜,但不会忘记以前的注册。Autofac 可以很好地处理同一个抽象的多个配置,但我们将在 13.4 节中回到这个主题。

Here, you register IIngredient twice. If you resolve IIngredient, you get an instance of Steak. The last registration wins, but previous registrations aren’t forgotten. Autofac handles multiple configurations for the same Abstraction well, but we’ll get back to this topic in section 13.4.

有更多高级选项可用于配置 Autofac,但您可以使用此处显示的方法配置整个应用程序。但是,为了避免对容器配置进行过多的显式维护,您可以考虑使用自动注册的更基于约定的方法。

There are more-advanced options available for configuring Autofac, but you can configure an entire application with the methods shown here. But to save yourself from too much explicit maintenance of container configuration, you could instead consider a more convention-based approach using Auto-Registration.

配置ContainerBuilder使用自动注册

Configuring the ContainerBuilder using Auto-Registration

在许多情况下,注册将是相似的。这样的注册维护起来很乏味,并且显式注册每个组件可能不是最有效的方法,正如我们在 12.3.3 节中讨论的那样。

In many cases, registrations will be similar. Such registrations are tedious to maintain, and explicitly registering each and every component might not be the most productive approach, as we discussed in section 12.3.3.

考虑一个包含许多IIngredient实现的库。您可以单独配置每个类,但这会导致对该方法进行大量看起来相似的调用RegisterType。更糟糕的是,每次添加新的实现时,如果您希望它可用IIngredient,还必须显式地注册它。声明应该注册在给定程序集中找到的ContainerBuilder所有实现会更有成效。IIngredient

Consider a library that contains many IIngredient implementations. You can configure each class individually, but it’ll result in numerous similar-looking calls to the RegisterType method. What’s worse is that every time you add a new IIngredient implementation, you must also explicitly register it with the ContainerBuilder if you want it to be available. It’d be more productive to state that all implementations of IIngredient found in a given assembly should be registered.

这可以使用该RegisterAssemblyTypes方法。此方法允许您指定一个程序集并将来自该程序集的所有选定类配置到单个语句中。要获取Assembly实例,您可以使用代表类(在本例中为Steak):

This is possible using the RegisterAssemblyTypes method. This method lets you specify an assembly and configure all selected classes from this assembly into a single statement. To get the Assembly instance, you can use a representative class (in this case, Steak):

Assembly ingredientsAssembly = typeof(Steak).Assembly;

builder.RegisterAssemblyTypes(ingredientsAssembly).As<IIngredient>();

RegisterAssemblyTypes方法_返回与方法相同的接口RegisterType,因此可以使用许多相同的配置选项。这是一个强大的功能,因为这意味着您无需学习新的 API 即可使用自动注册

The RegisterAssemblyTypes method returns the same interface as the RegisterType method, so many of the same configuration options are available. This is a strong feature, because it means that you don’t have to learn a new API to use Auto-Registration.

在前面的示例中,我们使用As方法将程序集中的所有类型注册为IIngredient服务。前面的示例还无条件地配置IIngredient接口的所有实现,但您可以提供过滤器,让您只选择一个子集。这是一个基于约定的扫描,您只添加名称以Sauce开头的类:

In the previous example, we used the As method to register all types in the assembly as IIngredient services. The previous example also unconditionally configures all implementations of the IIngredient interface, but you can provide filters that let you select only a subset. Here’s a convention-based scan where you add only classes whose name starts with Sauce:

Assembly ingredientsAssembly = typeof(Steak).Assembly;

builder.RegisterAssemblyTypes(ingredientsAssembly)
    .Where(type => type.Name.StartsWith("Sauce"))
    .As<IIngredient>();

在程序集中注册所有类型时,可以使用谓词来定义选择标准。与前面的代码示例的唯一区别是包含方法,您可以在其中仅选择名称以SauceWhere开头的那些类型。

When you register all types in an assembly, you can use a predicate to define a selection criterion. The only difference from the previous code example is the inclusion of the Where method, where you select only those types whose names start with Sauce.

还有许多其他方法可以让您提供各种选择标准。Where方法_为您提供了一个过滤器,它只允许那些与谓词匹配的类型通过,但还有一种Except方法可以反过来工作。

There are many other methods that let you provide various selection criteria. The Where method gives you a filter that only lets those types through that match the predicate, but there’s also an Except method that works the other way around.

除了从程序集中选择正确的类型外,自动注册的另一部分是定义正确的映射。在前面的示例中,我们使用As具有特定接口的方法来针对该接口注册所有选定的类型。但有时您会想要使用不同的约定。

Apart from selecting the correct types from an assembly, another part of Auto-Registration is defining the correct mapping. In the previous examples, we used the As method with a specific interface to register all selected types against that interface. But sometimes you’ll want to use different conventions.

假设您使用抽象基类而不是接口,并且您希望在名称以Policy结尾的程序集中注册所有类型。为此,该As方法还有其他几种重载,包括将 aFunc<Type, Type>作为输入的重载:

Let’s say that instead of interfaces, you use abstract base classes, and you want to register all types in an assembly where the name ends with Policy. For this purpose, there are several other overloads of the As method, including one that takes a Func<Type, Type> as input:

Assembly policiesAssembly = typeof(DiscountPolicy).Assembly;

builder.RegisterAssemblyTypes(policiesAssembly)
    .Where(type => type.Name.EndsWith("Policy"))
    .As(type => type.BaseType);

您可以为名称以PolicyAs结尾的每个类型使用提供给该方法的代码块。这确保所有带有Policy后缀的类将针对它们的基类进行注册,以便在请求基类时,容器会将其解析为按此约定映射的类型。使用 Autofac 进行基于约定的注册非常简单,并且使用的 API 与 singularRegisterType方法公开的 API 非常相似。

You can use the code block provided to the As method for every single type whose name ends with Policy. This ensures that all classes with the Policy suffix will be registered against their base class, so that when the base class is requested, the container will resolve it to the type mapped by this convention. Convention-based registration with Autofac is surprisingly easy and uses an API that closely mirrors the API exposed by the singular RegisterType method.

通用抽象的自动注册使用AsClosedTypesOf

Auto-Registration of generic Abstractions using AsClosedTypesOf

在第 10 章的过程中,您将大而令人讨厌IProductService的界面重构为ICommandService<TCommand>界面清单 10.12。这又是那个抽象

During the course of chapter 10, you refactored the big, obnoxious IProductService interface to the ICommandService<TCommand> interface of listing 10.12. Here’s that Abstraction again:

public interface ICommandService<TCommand>
{
    void Execute(TCommand command);
}

正如第 10 章所讨论的,每个命令参数对象都代表一个用例,除了任何实现横切关注点的装饰器之外,每个用例将有一个实现。以清单 10.8 为例。它实施了“调整库存”用例。下一个清单再次显示了这个类。AdjustInventoryService

As discussed in chapter 10, every command Parameter Object represents a use case and, apart from any Decorators that implement Cross-Cutting Concerns, there’ll be a single implementation per use case. The AdjustInventoryService of listing 10.8 was given as an example. It implemented the “adjust inventory” use case. The next listing shows this class again.

清单 13.2AdjustInventoryService来自第 10 章 的

Listing 13.2 The AdjustInventoryService from chapter 10

public class AdjustInventoryService : ICommandService<AdjustInventory>
{
    private readonly IInventoryRepository repository;

    public AdjustInventoryService(IInventoryRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(AdjustInventory command)
    {
        var productId = command.ProductId;

        ...
    }
}

任何相当复杂的系统都可以轻松实现数百个用例。这是使用Auto-Registration的理想选择。使用 Autofac,这再简单不过了,如以下清单所示。

Any reasonably complex system will easily implement hundreds of use cases. This is an ideal candidate for using Auto-Registration. With Autofac, this couldn’t be easier, as the following listing shows.

清单 13.3 实现的自动注册ICommandService<TCommand>

Listing 13.3 Auto-Registration of ICommandService<TCommand> implementations

Assembly assembly = typeof(AdjustInventoryService).Assembly;

builder.RegisterAssemblyTypes(assembly)
    .AsClosedTypesOf(typeof(ICommandService<>));

与前面的清单一样,您可以使用该RegisterAssemblyTypes方法从提供的程序集中选择类。As但是,您不是调用,而是调用AsClosedTypesOf并提供开放式通用ICommandService<TCommand>接口。

As in the previous listings, you make use of the RegisterAssemblyTypes method to select classes from the supplied assembly. Instead of calling As, however, you call AsClosedTypesOf and supply the open-generic ICommandService<TCommand> interface.

使用提供的开放通用接口,Autofac 遍历程序集类型列表并注册所有实现封闭通用版本的ICommandService<TCommand>. 例如,这意味着它AdjustInventoryService已注册,因为它实现ICommandService<AdjustInventory>了 ,这是 . 的封闭通用版本ICommandService<TCommand>

Using the supplied open-generic interface, Autofac iterates through the list of assembly types and registers all types that implement a closed-generic version of ICommandService<TCommand>. What this means, for instance, is that AdjustInventoryService is registered, because it implements ICommandService<AdjustInventory>, which is a closed-generic version of ICommandService<TCommand>.

RegisterAssemblyTypes方法_采用一paramsAssembly实例,因此您可以根据需要为单个约定提供尽可能多的程序集。扫描文件夹中的程序集并提供所有程序集以实现加载项功能并不是一个牵强附会的想法。这样,无需重新编译核心应用程序即可添加插件。这是实现后期绑定的一种方式;另一种是使用配置文件。

The RegisterAssemblyTypes method takes a params array of Assembly instances, so you can supply as many assemblies to a single convention as you’d like. It’s not a far-fetched thought to scan a folder for assemblies and supply them all to implement add-in functionality. In that way, add-ins can be added without recompiling a core application. This is one way to implement late binding; another is to use configuration files.

配置ContainerBuilder使用配置文件

Configuring the ContainerBuilder using configuration files

当您需要在不重新编译应用程序的情况下更改容器的注册时,配置文件是一个可行的选择。正如我们在 12.2.1 节中所述,您应该仅将配置文件用于需要后期绑定的那些类型的 DI 配置:在所有其他类型和配置的所有其他部分中更喜欢配置为代码自动注册。

When you need to change a container’s registrations without recompiling the application, configuration files are a viable option. As we stated in section 12.2.1, you should use configuration files only for those types of your DI configuration that require late binding: prefer Configuration as Code or Auto-Registration in all other types and all other parts of your configuration.

使用配置文件的最自然方式是将它们嵌入到标准 .NET 应用程序配置文件中。这是可能的,但如果您需要独立于标准 .config 文件改变 Autofac 配置,您也可以使用独立的配置文件。无论您想做其中之一,API 几乎是一样的。

The most natural way to use configuration files is to embed those into the standard .NET application configuration file. This is possible, but you can also use a standalone configuration file if you need to vary the Autofac configuration independently of the standard .config file. Whether you want to do one or the other, the API is almost the same.

一旦获得对 的引用Autofac.Configuration,您就可以请求ContainerBuilder从标准 .config 文件中读取组件注册,如下所示:

Once you have a reference to Autofac.Configuration, you can ask the ContainerBuilder to read component registrations from the standard .config file like this:

var configuration = new ConfigurationBuilder()    ①  
    .AddJsonFile("autofac.json")    ①  
    .Build();    ①  

builder.RegisterModule(    ②  
    new ConfigurationModule(configuration));    ②  

这是一个将IIngredient接口映射到Steak类的简单示例:

Here’s a simple example that maps the IIngredient interface to the Steak class:

{
  "defaultAssembly": "Ploeh.Samples.MenuModel",    ①  
  "components": [
  {
    "services": [{    ②  
      "type": "Ploeh.Samples.MenuModel.IIngredient"    ②  
    }],    ②  
    "type": "Ploeh.Samples.MenuModel.Steak"    ②  
  }]
}

类型名称必须包含命名空间,以便 Autofac 可以找到该类型。因为这两种类型都位于默认程序集中Ploeh.Samples.MenuModel,所以在这种情况下可以省略程序集名称。虽然defaultAssembly属性可选,这是一个很好的特性,如果您在同一个程序集中定义了很多类型,它可以让您免于大量输入。

The type name must include the namespace so that Autofac can find that type. Because both types are located in the default assembly Ploeh.Samples.MenuModel, the assembly name can be omitted in this case. Although the defaultAssembly attribute is optional, it’s a nice feature that can save you from a lot of typing if you have many types defined in the same assembly.

components元素是一个 JSONcomponent元素数组. 前面的示例包含单个组件,但您可以添加任意数量的组件元素。在每个元素中,您必须指定具有type属性的具体类型。这是唯一必需的属性。要将Steak类映射到IIngredient,您可以使用可选services属性。

The components element is a JSON array of component elements. The previous example contained a single component, but you can add as many component elements as you like. In each element, you must specify a concrete type with the type attribute. This is the only required attribute. To map the Steak class to IIngredient, you can use the optional services attribute.

当您需要在不重新编译应用程序的情况下更改一个或多个组件的配置时,配置文件是一个不错的选择,但由于它往往非常脆弱,因此您应该只在这些情况下保留它。使用自动注册配置作为容器配置的主要部分的代码。

A configuration file is a good option when you need to change the configuration of one or more components without recompiling the application, but because it tends to be quite brittle, you should reserve it for only those occasions. Use either Auto-Registration or Configuration as Code for the main part of the container’s configuration.

本节介绍了 Autofac DI 容器并演示了这些基本机制:如何配置ContainerBuilder以及随后如何使用构建的容器来解析服务。只需调用一次Resolve方法即可轻松完成解析服务,因此复杂性涉及配置容器。这可以通过几种不同的方式完成,包括命令式代码和配置文件。

This section introduced the Autofac DI Container and demonstrated these fundamental mechanics: how to configure a ContainerBuilder and, subsequently, how to use the constructed container to resolve services. Resolving services is easily done with a single call to the Resolve method, so the complexity involves configuring the container. This can be done in several different ways, including imperative code and configuration files.

到目前为止,我们只了解了最基本的 API,因此我们还没有涉及更高级的领域。最重要的主题之一是如何管理组件的生命周期。

Until now, we’ve only looked at the most basic API, so there are more-advanced areas we have yet to cover. One of the most important topics is how to manage component lifetime.

13.2 管理生命周期

13.2 Managing lifetime

在第 8 章中,我们讨论了生命周期管理,包括最常见的概念生命周期,例如SingletonScopedTransient。Autofac 支持多种不同的Lifestyles,使您能够配置所有服务的生命周期。表 13.2中显示的生活方式作为 API 的一部分提供。

In chapter 8, we discussed Lifetime Management, including the most common conceptual Lifestyles such as Singleton, Scoped, and Transient. Autofac supports several different Lifestyles, enabling you to configure the lifetime of all services. The Lifestyles shown in table 13.2 are available as part of the API.

表 13.2 Autofac 实例范围(生活方式
Autofac 名称花样名称注释
每个依赖短暂的这是默认的实例范围。实例由容器跟踪。
单实例单例实例在容器被处置时被处置。
每生命周期范围范围将组件的生命周期与生命周期范围联系在一起(参见第 13.2.1 节)。

Autofac 的TransientSingleton实现等同于第 8 章中描述的一般Lifestyles,因此我们在本章中不会花太多时间在它们上。相反,在本节中,您将了解如何在代码和配置文件中为组件定义Lifestyles 。我们还将了解 Autofac 的生命周期概念以及如何使用它们来实现Scoped Lifestyle。到本节结束时,您应该能够在自己的应用程序中使用 Autofac 的Lifestyles 。让我们首先回顾一下如何为组件配置实例范围。

Autofac’s implementations of Transient and Singleton are equivalent to the general Lifestyles described in chapter 8, so we won’t spend much time on them in this chapter. Instead, in this section, you’ll see how you can define Lifestyles for components both in code and with configuration files. We’ll also look at Autofac’s concept of lifetime scopes and how they can be used to implement the Scoped Lifestyle. By the end of this section, you should be able to use Autofac’s Lifestyles in your own application. Let’s start by reviewing how to configure instance scopes for components.

13.2.1 配置实例作用域

13.2.1 Configuring instance scopes

在本节中,我们将回顾如何使用 Autofac 管理组件实例范围。实例范围被配置为注册组件的一部分,您可以使用代码和配置文件来定义它们。我们将依次查看每一个。

In this section, we’ll review how to manage component instance scopes with Autofac. Instance scopes are configured as part of registering components, and you can define them both with code and via a configuration file. We’ll look at each in turn.

使用代码配置实例范围

Configuring instance scopes with code

实例范围定义为您在ContainerBuilder实例上进行的注册的一部分。就这么简单:

Instance scope is defined as part of the registrations you make on a ContainerBuilder instance. It’s as easy as this:

builder.RegisterType<SauceBéarnaise>().SingleInstance();

这将具体SauceBéarnaise类配置为单例,以便每次SauceBéarnaise请求时返回相同的实例。如果要将抽象映射到具有特定生命周期的具体类,可以使用通常As的方法并将方法SingleInstance放置随叫随到。这两个注册在功能上是等价的:

This configures the concrete SauceBéarnaise class as a Singleton so that the same instance is returned each time SauceBéarnaise is requested. If you want to map an Abstraction to a concrete class with a specific lifetime, you can use the usual As method and place the SingleInstance method call wherever you like. These two registrations are functionally equivalent:

builder                                builder
    .RegisterType<SauceBéarnaise>()        .RegisterType<SauceBéarnaise>()
    .As<IIngredient>()                     .SingleInstance()
    .SingleInstance();                     .As<IIngredient>();

请注意,唯一的区别是我们交换了AsSingleInstance方法调用。就个人而言,我们更喜欢左边的序列,因为RegisterTypeAs方法调用形成了具体类和抽象类之间的映射。将它们保持在一起可以使注册更具可读性,然后您可以将实例范围声明为对映射的修改。

Notice that the only difference is that we’ve swapped the As and SingleInstance method calls. Personally, we prefer the sequence on the left, because the RegisterType and As method calls form a mapping between a concrete class and an Abstraction. Keeping them close together makes the registration more readable, and you can then state the instance scope as a modification to the mapping.

尽管Transient是默认的实例范围,但您可以显式声明它。这两个例子是等价的:

Although Transient is the default instance scope, you can explicitly state it. These two examples are equivalent:

builder                                builder
    .RegisterType<SauceBéarnaise>();       .RegisterType<SauceBéarnaise>()
                                           .InstancePerDependency();

使用与单一注册相同的方法为基于约定的注册配置实例范围:

Configuring instance scope for convention-based registrations is done using the same method as for singular registrations:

Assembly ingredientsAssembly = typeof(Steak).Assembly;

builder.RegisterAssemblyTypes(ingredientsAssembly).As<IIngredient>()
    .SingleInstance();

您可以使用SingleInstance和其他相关方法来定义约定中所有注册的实例范围。在前面的示例中,您将所有IIngredient注册定义为Singleton。就像您可以在代码和配置文件中注册组件一样,您也可以在这两个地方配置实例范围。

You can use SingleInstance and the other related methods to define the instance scope for all registrations in a convention. In the previous example, you defined all IIngredient registrations as Singleton. In the same way that you can register components both in code and in a configuration file, you can also configure instance scope in both places.

使用配置文件配置实例范围

Configuring instance scopes with configuration files

当您需要在配置文件中定义组件时,您可能希望在同一个地方配置它们的实例范围;否则,它会导致所有组件都获得相同的默认Lifestyle。作为您在 13.1.2 节中看到的配置模式的一部分,这很容易完成。您可以使用可选instance-scope属性声明Lifestyle

When you need to define components in a configuration file, you might want to configure their instance scopes in the same place; otherwise, it would result in all components getting the same default Lifestyle. This is easily done as part of the configuration schema you saw in section 13.1.2. You can use the optional instance-scope attribute to declare the Lifestyle.

清单 13.4 使用可选instance-scope属性

Listing 13.4 Using the optional instance-scope attribute

{
  "defaultAssembly": "Ploeh.Samples.MenuModel",
  "components": [
  {
    "services": [{
      "type": "Ploeh.Samples.MenuModel.IIngredient"
    }],
    "type": "Ploeh.Samples.MenuModel.Steak",
    "instance-scope": "single-instance"    ①  
  }]
}

与 13.1.2 节中的示例相比,唯一的区别是添加instance-scope了将实例配置为Singleton的属性。当您省略instance-scope属性时,per-dependency使用 Autofac 等同于Transient的属性。

Compared to the example in section 13.1.2, the only difference is the added instance-scope attribute that configures the instance as a Singleton. When you omit the instance-scope attribute, per-dependency is used, which is Autofac’s equivalent to Transient.

在代码和文件中,很容易为组件配置实例范围。在所有情况下,它都是以一种相当声明的方式完成的。尽管配置很容易,但您一定不要忘记,一些Lifestyles涉及长期存在的对象,只要它们存在就使用资源。

Both in code and in a file, it’s easy to configure instance scopes for components. In all cases, it’s done in a rather declarative fashion. Although configuration is easy, you must not forget that some Lifestyles involve long-lived objects that use resources as long as they’re around.

13.2.2 释放组件

13.2.2 Releasing components

正如 8.2.2 节中所讨论的,当你用完它们时释放对象是很重要的。Autofac 没有明确Release的方法,而是使用一个称为生命周期范围的概念. 生命周期范围可以被视为容器的一次性副本。如图13.3所示,它定义了组件可以重用的边界。

As discussed in section 8.2.2, it’s important to release objects when you’re done with them. Autofac has no explicit Release method but instead uses a concept called lifetime scopes. A lifetime scope can be regarded as a throw-away copy of the container. As figure 13.3 illustrates, it defines a boundary where components can be reused.

13-03.eps

图 13.3 Autofac 的生命周期范围充当容器,可以在有限的持续时间或目的内共享组件。

Figure 13.3 Autofac’s lifetime scopes act as containers that can share components for a limited duration or purpose.

生命周期范围定义了一个派生容器,您可以将其用于特定的持续时间或目的;最明显的例子是网络请求。您从容器中生成一个作用域,以便该作用域继承父容器跟踪的所有单例,但该作用域也充当本地单例的容器。当从生命周期范围请求生命周期范围的组件时,您总是会收到相同的实例。与真正的单例的不同之处在于,如果您查询第二个范围,您将获得另一个实例。

A lifetime scope defines a derived container that you can use for a particular duration or purpose; the most obvious example is a web request. You spawn a scope from a container so that the scope inherits all the Singletons tracked by the parent container, but the scope also acts as a container of local Singletons. When a lifetime-scoped component is requested from a lifetime scope, you always receive the same instance. The difference from true Singletons is that if you query a second scope, you’ll get another instance.

生命周期范围的重要特性之一是它们允许您在范围完成时正确释放组件。BeginLifetimeScope您使用该方法创建一个新范围Dispose并通过像这样调用其方法来释放所有适当的组件:

One of the important features of lifetime scopes is that they allow you to properly release components when the scope completes. You create a new scope with the BeginLifetimeScope method and release all appropriate components by invoking its Dispose method like so:

using (var scope = container.BeginLifetimeScope())    ①  
{
    IMeal meal = scope.Resolve<IMeal>();    ②  

    meal.Consume();    ③  

}    ④  

BeginLifetimeScope您可以通过调用该方法从容器中创建一个新范围。返回值实现IDisposable,因此您可以将其包装在一个using块中。因为它也实现了容器本身实现的相同接口,所以您可以使用作用域以与容器本身完全相同的方式解析组件。

You create a new scope from the container by invoking the BeginLifetimeScope method. The return value implements IDisposable so you can wrap it in a using block. Because it also implements the same interface that the container itself implements, you can use the scope to resolve components in exactly the same way as with the container itself.

完成生命周期范围后,您可以将其处理掉。using当您退出块时,块会自动发生这种情况,但您也可以选择通过调用该Dispose方法来显式处理它。当您处理范围时,您还释放了生命周期范围创建的所有组件。在这个例子中,这意味着你释放了餐点对象图。

When you’re done with a lifetime scope, you can dispose of it. This happens automatically with a using block when you exit the block, but you can also choose to explicitly dispose of it by invoking the Dispose method. When you dispose of a scope, you also release all the components that were created by the lifetime scope. In the example, it means that you release the meal object graph.

组件的依赖关系总是在组件的生命周期范围内或以下解决。例如,如果您需要将Transient Dependency注入到Singleton中,该Transient Dependency来自根容器,即使您从嵌套的生命周期范围解析Singleton 。这将跟踪根容器内的Transient并防止在生命周期范围被处理时将其处理掉。否则,Singleton消费者会崩溃,因为它在根容器中保持活动状态,同时依赖于已处理的组件。

Dependencies of a component are always resolved at or below the component’s lifetime scope. For example, if you need a Transient Dependency injected into a Singleton, that Transient Dependency comes from the root container even if you’re resolving the Singleton from a nested lifetime scope. This will track the Transient within the root container and prevent it from being disposed of when the lifetime scope gets disposed of. The Singleton consumer would otherwise break, because it’s kept alive in the root container while depending on a component that was disposed of.

在本节的前面,您看到了如何将组件配置为SingletonsTransients。将组件配置为将其实例范围绑定到生命周期范围以类似的方式完成:

Earlier in this section, you saw how to configure components as Singletons or Transients. Configuring a component to have its instance scope tied to a lifetime scope is done in a similar way:

builder.RegisterType<SauceBéarnaise>()
    .As<IIngredient>()
    .InstancePerLifetimeScope();    ①  

由于它们的性质,单例在容器本身的生命周期内永远不会被释放。尽管如此,如果您不再需要容器,您甚至可以释放这些组件。这是通过处理容器本身来完成的:

Due to their nature, Singletons are never released for the lifetime of the container itself. Still, you can release even those components if you don’t need the container any longer. This is done by disposing of the container itself:

container.Dispose();

实际上,这并不像处理范围那么重要,因为容器的生命周期往往与其支持的应用程序的生命周期密切相关。只要应用程序运行,您通常会保留容器,因此您只会在应用程序关闭时处理它。在这种情况下,内存将被操作系统回收。

In practice, this isn’t nearly as important as disposing of a scope because the lifetime of a container tends to correlate closely with the lifetime of the application it supports. You normally keep the container around as long as the application runs, so you’d only dispose of it when the application shuts down. In this case, memory would be reclaimed by the operating system.

我们的Autofac生命周期管理之旅到此结束。组件可以配置为混合实例范围,即使你注册了同一个抽象的多个实现也是如此。但直到现在,您已经通过隐式假设所有组件都使用Auto-Wiring来允许容器连接依赖项。情况并非总是如此。在下一节中,我们将回顾如何处理必须以特殊方式实例化的类。

This completes our tour of Lifetime Management with Autofac. Components can be configured with mixed instance scopes, and this is true even when you register multiple implementations of the same Abstraction. But until now, you’ve allowed the container to wire Dependencies by implicitly assuming that all components use Auto-Wiring. This isn’t always the case. In the next section, we’ll review how to deal with classes that must be instantiated in special ways.

13.3 注册困难的 API

13.3 Registering difficult APIs

到目前为止,我们已经考虑了如何配置使用构造函数注入的组件。构造函数注入的众多好处之一是像 Autofac 这样的DI 容器可以很容易地理解如何在依赖图中组合和创建所有类。当 API 表现不佳时,这一点就不太清楚了。

Until now, we’ve considered how you can configure components that use Constructor Injection. One of the many benefits of Constructor Injection is that DI Containers like Autofac can easily understand how to compose and create all classes in a Dependency graph. This becomes less clear when APIs are less well behaved.

在本节中,您将看到如何处理原始构造函数参数和静态工厂。这些都需要特别注意。让我们首先看一下采用基本类型(例如字符串或整数)作为构造函数参数的类。

In this section, you’ll see how to deal with primitive constructor arguments and static factories. These all require special attention. Let’s start by looking at classes that take primitive types, such as strings or integers, as constructor arguments.

13.3.1 配置原始依赖

13.3.1 Configuring primitive Dependencies

只要您将抽象注入消费者,一切都很好。但是,当构造函数依赖于基本类型(例如字符串、数字或枚举)时,这就变得更加困难。对于将连接字符串作为构造函数参数的数据访问实现尤其如此,但这是适用于所有字符串和数字的更普遍的问题。

As long as you inject Abstractions into consumers, all is well. But it becomes more difficult when a constructor depends on a primitive type, such as a string, a number, or an enum. This is particularly the case for data access implementations that take a connection string as constructor parameter, but it’s a more general issue that applies to all strings and numbers.

从概念上讲,将字符串或数字注册为容器中的组件并不总是有意义的。但是有了 Autofac,这至少是可能的。以这个构造函数为例:

Conceptually, it doesn’t always make sense to register a string or number as a component in a container. But with Autofac, this is at least possible. Consider as an example this constructor:

public ChiliConCarne(Spiciness spiciness)

在这个例子中,Spiciness是一个枚举:

In this example, Spiciness is an enum:

public enum Spiciness { Mild, Medium, Hot }

如果你想让所有的消费者Spiciness使用相同的值,你可以Spiciness互相ChiliConCarne独立注册。这个片段展示了如何:

If you want all consumers of Spiciness to use the same value, you can register Spiciness and ChiliConCarne independently of each other. This snippet shows how:

builder.Register<Spiciness>(c => Spiciness.Medium);
builder.RegisterType<ChiliConCarne>().As<ICourse>();

当您随后 resolveChiliConCarne时,它​​的Spiciness值为Medium,所有其他依赖于的组件也是如此SpicinessChiliConCarne如果您想在更精细的层面上控制和之间的关系Spiciness,则可以使用该WithParameter方法。因为你想为Spiciness参数提供一个具体的值,你可以使用WithParameter带有参数名称和值的重载:

When you subsequently resolve ChiliConCarne, it’ll have a Spiciness value of Medium, as will all other components with a Dependency on Spiciness. If you’d rather control the relationship between ChiliConCarne and Spiciness on a finer level, you can use the WithParameter method. Because you want to supply a concrete value for the Spiciness parameter, you can use the WithParameter overload that takes a parameter name and a value:

builder.RegisterType<ChiliConCarne>().As<ICourse>()
.WithParameter(
        "spiciness",    ①  
        Spiciness.Hot);    ②  

此处描述的两个选项都使用自动接线为组件提供具体值。正如 13.4 节中所讨论的,这有优点也有缺点。然而,更方便的解决方案是将原始依赖项提取到参数对象中。

Both options described here use Auto-Wiring to provide a concrete value to a component. As discussed in section 13.4, this has advantages and disadvantages. A more convenient solution, however, is to extract the primitive Dependencies into Parameter Objects.

在 10.3.3 节中,我们讨论了参数对象的引入如何允许缓解开闭原则造成的违规行为IProductService。然而,参数对象也是减少歧义的好工具。

In section 10.3.3, we discussed how the introduction of Parameter Objects allowed mitigating the Open/Closed Principle violation that IProductService caused. Parameter Objects, however, are also a great tool to mitigate ambiguity.

例如Spiciness,一道菜的味道可以用更笼统的术语“调味”来描述。调味可能包括其他特性,例如咸味。换句话说,您可以将Spicinessand包装ExtraSalty在一个Flavoring类中:4 

The Spiciness of a course, for instance, could be described in the more general term flavoring. Flavoring might include other properties, such as saltiness. In other words, you can wrap the Spiciness and ExtraSalty in a Flavoring class:4 

public class Flavoring
{
    public readonly Spiciness Spiciness;
    public readonly bool ExtraSalty;

    public Flavoring(Spiciness spiciness, bool extraSalty)
    {
        this.Spiciness = spiciness;
        this.ExtraSalty = extraSalty;
    }
}

随着Flavoring参数对象的引入,现在可以很容易地自动连接任何ICourse需要一些修饰的实现:

With the introduction of the Flavoring Parameter Object, it now becomes easy to Auto-Wire any ICourse implementation that requires some flavoring:

var flavoring = new Flavoring(Spiciness.Medium, extraSalty: true);
builder.RegisterInstance<Flavoring>(flavoring);

builder.RegisterType<ChiliConCarne>().As<ICourse>();

现在你有一个Flavoring类的实例。Flavoring成为ICourses 的配置对象。因为只有一个Flavoring实例,所以您可以使用RegisterInstance.

Now you have a single instance of the Flavoring class. Flavoring becomes a configuration object for ICourses. Because there’ll only be one Flavoring instance, you can register it in Autofac using RegisterInstance.

将原始依赖项提取到参数对象中应该是您优先于前面讨论的选项,因为参数对象消除了功能和技术级别的歧义。但是,它确实需要更改组件的构造函数,这可能并不总是可行的。在这种情况下,使用WithParameter是您的第二佳选择。

Extracting primitive Dependencies into Parameter Objects should be your preference over the previously discussed options because Parameter Objects remove ambiguity, both at the functional and the technical levels. It does, however, require a change to a component’s constructor, which might not always be feasible. In this case, the use of WithParameter is your second-best pick.

13.3.2 用代码块注册对象

13.3.2 Registering objects with code blocks

创建具有原始值的组件的另一种选择是使用Register方法。它允许您提供创建组件的委托:

Another option for creating a component with a primitive value is to use the Register method. It lets you supply a delegate that creates the component:

builder.Register<ICourse>(c => new ChiliConCarne(Spiciness.Hot));

你已经看到了这个Register方法当我们Spiciness在 13.3.1 节讨论注册时。在这里,每次解析服务时都会ChiliConCarne调用构造函数,其Spiciness值为。HotICourse

You already saw the Register method when we discussed the registration of Spiciness in section 13.3.1. Here, the ChiliConCarne constructor is invoked with a Spiciness value of Hot every time the ICourse service is resolved.

谈到ChiliConCarne类时,您可以选择自动装配或使用代码块。其他类可能更严格:它们不能通过公共构造函数实例化。相反,您必须使用某种工厂来创建该类型的实例。这对于DI 容器来说总是很麻烦,因为默认情况下,它们会照看公共构造函数。考虑公共JunkFood类的这个示例构造函数:

When it comes to the ChiliConCarne class, you have a choice between Auto-Wiring or using a code block. Other classes can be more restrictive: they can’t be instantiated through a public constructor. Instead, you must use some sort of factory to create instances of the type. This is always troublesome for DI Containers, because, by default, they look after public constructors. Consider this example constructor for the public JunkFood class:

internal JunkFood(string name)

尽管JunkFood该类可能是公共的,但构造函数是内部的。在此示例中,JunkFood应该通过静态JunkFoodFactory类创建实例:

Even though the JunkFood class might be public, the constructor is internal. In this example, instances of JunkFood should instead be created through the static JunkFoodFactory class:

public static class JunkFoodFactory
{
    public static JunkFood Create(string name)
    {
        return new JunkFood(name);
    }
}

从 Autofac 的角度来看,这是一个有问题的 API,因为围绕静态工厂没有明确且完善的约定。它需要帮助——您可以通过提供它可以执行以创建实例的代码块来提供帮助:

From Autofac’s perspective, this is a problematic API, because there are no unambiguous and well-established conventions around static factories. It needs help — and you can give that help by providing a code block it can execute to create the instance:

builder.Register<IMeal>(c => JunkFoodFactory.Create("chicken meal"));

这一次,您使用该Register方法通过在代码块中调用静态工厂来创建组件。有了它,JunkFoodFactory.Create每次IMeal解析并返回结果时都会调用它。

This time, you use the Register method to create the component by invoking a static factory within the code block. With that in place, JunkFoodFactory.Create is invoked every time IMeal is resolved and the result returned.

当您最终编写创建实例的代码时,这比直接调用代码有何优势?Register通过在方法调用中使用代码块,您仍然可以获得一些东西:

When you end up writing the code to create the instance, how is this better than invoking the code directly? By using a code block inside a Register method call, you still gain something:

  • 你映射从IMealJunkFood。这允许消费类保持松散耦合。
  • You map from IMeal to JunkFood. This allows consuming classes to stay loosely coupled.
  • 仍然可以配置实例范围。虽然会调用代码块来创建实例,但可能不会在每次请求实例时都调用它。它是默认设置,但如果将其更改为Singleton,则代码块将仅被调用一次,结果会被缓存并在之后重用。
  • Instance scope can still be configured. Although the code block will be invoked to create the instance, it may not be invoked every time the instance is requested. It is by default, but if you change it to a Singleton, the code block will only be invoked once, with the result cached and reused thereafter.

在本节中,您了解了如何使用 Autofac 来处理更难创建的 API。您可以使用该WithParameter方法使用服务连接构造函数以保持Auto-Wiring的外观,或者您可以将Register方法与代码块一起使用以获得更类型安全的方法。我们还没有研究如何使用多个组件,所以现在让我们把注意力转向那个方向。

In this section, you’ve seen how you can use Autofac to deal with more-difficult creational APIs. You can use the WithParameter method to wire constructors with services to maintain a semblance of Auto-Wiring, or you can use the Register method with a code block for a more type-safe approach. We have yet to look at how to work with multiple components, so let’s now turn our attention in that direction.

13.4 使用多个组件

13.4 Working with multiple components

正如在 12.1.2 节中提到的,DI 容器在独特性上茁壮成长,但在模棱两可的情况下却很难。使用Constructor Injection时,单个构造函数优于重载构造函数,因为在别无选择时使用哪个构造函数是显而易见的。从抽象映射到具体类型时也是如此。如果您试图将多个具体类型映射到同一个抽象,就会引入歧义。

As alluded to in section 12.1.2, DI Containers thrive on distinctness but have a hard time with ambiguity. When using Constructor Injection, a single constructor is preferred over overloaded constructors, because it’s evident which constructor to use when there’s no choice. This is also the case when mapping from Abstractions to concrete types. If you attempt to map multiple concrete types to the same Abstraction, you introduce ambiguity.

尽管模棱两可的性质不受欢迎,但您经常需要处理单个抽象的多个实现。5  在以下情况下可能会出现这种情况:

Despite the undesirable qualities of ambiguity, you often need to work with multiple implementations of a single Abstraction.5  This can be the case in situations like these:

  • 不同的混凝土类型用于不同的消费者。
  • Different concrete types are used for different consumers.
  • 依赖关系是序列。
  • Dependencies are sequences.
  • 正在使用装饰器或复合材料。
  • Decorators or Composites are in use.

在本节中,我们将查看这些案例中的每一个,并了解 Autofac 如何依次解决每个问题。当我们完成后,您应该能够注册和解析组件,即使同一抽象的多个实现正在运行。让我们首先看看如何提供比Auto-Wiring提供的更细粒度的控制。

In this section, we’ll look at each of these cases and see how Autofac addresses each in turn. When we’re done, you should be able to register and resolve components even when multiple implementations of the same Abstraction are in play. Let’s first see how you can provide more fine-grained control than Auto-Wiring provides.

13.4.1 在多个候选人中选择

13.4.1 Selecting among multiple candidates

自动接线方便且功能强大,但提供的控制很少。只要所有抽象明确映射到具体类型,就没有问题。但是,一旦您引入了同一接口的更多实现,歧义就会浮出水面。让我们首先回顾一下 Autofac 如何处理同一个抽象的多个注册。

Auto-Wiring is convenient and powerful but provides little control. As long as all Abstractions are distinctly mapped to concrete types, you have no problems. But as soon as you introduce more implementations of the same interface, ambiguity rears its ugly head. Let’s first recap how Autofac deals with multiple registrations of the same Abstraction.

配置同一服务的多个实现

Configuring multiple implementations of the same service

正如您在 13.1.2 节中看到的,您可以像这样注册同一接口的多个实现:

As you saw in section 13.1.2, you can register multiple implementations of the same interface like this:

builder.RegisterType<Steak>().As<IIngredient>();
builder.RegisterType<SauceBéarnaise>().As<IIngredient>();

此示例将SteakSauceBéarnaise类注册为IIngredient服务。最后一次注册获胜,因此如果您使用 解决IIngredientscope.Resolve<IIngredient>()您将获得一个SauceBéarnaise实例。

This example registers both the Steak and SauceBéarnaise classes as the IIngredient service. The last registration wins, so if you resolve IIngredient with scope.Resolve<IIngredient>(), you’ll get a SauceBéarnaise instance.

您也可以要求容器解析所有IIngredient组件。Autofac 没有专门的方法来做到这一点,而是依赖于关系类型(https://mng.bz/P429)。关系类型是指示容器可以解释的关系的类型。例如,您可以使用IEnumerable<T>来指示您需要给定类型的所有服务:

You can also ask the container to resolve all IIngredient components. Autofac has no dedicated method to do that, but instead relies on relationship types (https://mng.bz/P429). A relationship type is a type that indicates a relationship that the container can interpret. As an example, you can use IEnumerable<T> to indicate that you want all services of a given type:

IEnumerable<IIngredient> ingredients =
    scope.Resolve<IEnumerable<IIngredient>>();

请注意,我们使用的是普通Resolve方法,但我们请求IEnumerable<IIngredient>. Autofac 将此解释为约定,并为我们提供了IIngredient它拥有的所有组件。

Notice that we use the normal Resolve method, but that we request IEnumerable<IIngredient>. Autofac interprets this as a convention and gives us all the IIngredient components it has.

当您注册组件时,您可以为每个注册指定一个名称,稍后您可以使用该名称在不同的组件中进行选择。此代码片段显示了该过程:

When you register components, you can give each registration a name that you can later use to select among the different components. This code snippet shows that process:

builder.RegisterType<Steak>().Named<IIngredient>("meat");
builder.RegisterType<SauceBéarnaise>().Named<IIngredient>("sauce");

与往常一样,您从RegisterType方法开始,而不是跟进As方法,你使用的Named方法指定服务类型和名称。这使您能够通过向方法提供相同的名称来解析命名服务ResolveNamed

As always, you start with the RegisterType method, but instead of following up with the As method, you use the Named method to specify a service type as well as a name. This enables you to resolve named services by supplying the same name to the ResolveNamed method:

IIngredient meat = scope.ResolveNamed<IIngredient>("meat");
IIngredient sauce = scope.ResolveNamed<IIngredient>("sauce");

用字符串命名组件是DI 容器的一个相当普遍的特性。但 Autofac 还允许您使用任意键识别组件:

Naming components with strings is a fairly common feature of DI Containers. But Autofac also lets you identify components with arbitrary keys:

object meatKey = new object();
builder.RegisterType<Steak>().Keyed<IIngredient>(meatKey);

键可以是任何对象,您随后可以使用它来解析组件:

The key can be any object, and you can subsequently use it to resolve the component:

IIngredient meat = scope.ResolveKeyed<IIngredient>(meatKey);

鉴于您应该始终在单个Composition Root中解析服务,因此您通常不应期望在此级别上处理此类歧义。如果您确实发现自己Resolve使用特定名称或键调用该方法,请考虑是否可以更改您的方法以减少歧义。在为给定服务配置依赖项时,您还可以使用命名或键控实例在多个备选方案中进行选择。

Given that you should always resolve services in a single Composition Root, you should normally not expect to deal with such ambiguity on this level. If you do find yourself invoking the Resolve method with a specific name or key, consider if you can change your approach to be less ambiguous. You can also use named or keyed instances to select among multiple alternatives when configuring Dependencies for a given service.

注册命名依赖

Registering named Dependencies

与自动装配一样有用,有时您需要覆盖正常行为以提供对哪些依赖项去往何处的细粒度控制;也可能是您需要解决一个不明确的 API。例如,考虑这个构造函数:

As useful as Auto-Wiring is, sometimes you need to override the normal behavior to provide fine-grained control over which Dependencies go where; it can also be that you need to address an ambiguous API. As an example, consider this constructor:

public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)

在这种情况下,您有三个相同类型的Dependencies,每个代表一个不同的概念。在大多数情况下,您希望将每个依赖项映射到一个单独的类型。以下清单显示了您可以如何选择注册ICourse映射。

In this case, you have three identically typed Dependencies, each of which represents a different concept. In most cases, you want to map each of the Dependencies to a separate type. The following listing shows how you could choose to register the ICourse mappings.

清单 13.5 注册命名课程

Listing 13.5 Registering named courses

builder.RegisterType<Rillettes>().Named<ICourse>("entrée");
builder.RegisterType<CordonBleu>().Named<ICourse>("mainCourse");
builder.RegisterType<MousseAuChocolat>().Named<ICourse>("dessert");

在这里,您注册了三个命名组件,映射Rilettes到名为 entrée 的实例映射到CordonBleu名为mainCourse的实例,以及映射到名为dessertMousseAuChocolat的实例。鉴于此配置,您现在可以使用命名注册来注册类。ThreeCourseMeal

Here, you register three named components, mapping the Rilettes to an instance named entrée, CordonBleu to an instance named mainCourse, and the MousseAuChocolat to an instance named dessert. Given this configuration, you can now register the ThreeCourseMeal class with the named registrations.

事实证明这非常复杂。在下面的清单中,我们将首先向您展示它的外观,然后我们将随后拆解示例以了解发生了什么。

This turns out to be surprisingly complex. In the following listing, we’ll first show you what it looks like, and then we’ll subsequently pick apart the example to understand what’s going on.

清单 13.6 覆盖自动装配

Listing 13.6 Overriding Auto-Wiring

builder.RegisterType<ThreeCourseMeal>().As<IMeal>()
    .WithParameter(    ①  
        (p, c) => p.Name == "entrée",
        (p, c) => c.ResolveNamed<ICourse>("entrée"))
    .WithParameter(
        (p, c) => p.Name == "mainCourse",    ②  
        (p, c) => c.ResolveNamed<ICourse>("mainCourse"))
    .WithParameter(
        (p, c) => p.Name == "dessert",
        (p, c) => c.ResolveNamed<ICourse>("dessert"));    ③  

让我们仔细看看这里发生了什么。WithParameter方法_重载环绕ResolvedParameter班级,它有这个构造函数:

Let’s take a closer look at what’s going on here. The WithParameter method overload wraps around the ResolvedParameter class, which has this constructor:

public ResolvedParameter(
    Func<ParameterInfo, IComponentContext, bool> predicate,
    Func<ParameterInfo, IComponentContext, object> valueAccessor);

predicate参数是一个测试,用于确定valueAccessor委托是否将被调用。predicate返回时,true调用valueAccessor以提供参数值。两个委托都采用相同的输入:关于ParameterInfo对象形式的参数信息和IComponentContext可用于解析其他组件的 。当 Autofac 使用ResolvedParameter实例时,它会在调用委托时提供这两个值。

The predicate parameter is a test that determines whether the valueAccessor delegate will be invoked. When predicate returns true, valueAccessor is invoked to provide the value for the parameter. Both delegates take the same input: information about the parameter in the form of a ParameterInfo object and an IComponentContext that can be used to resolve other components. When Autofac uses the ResolvedParameter instances, it provides both of these values when it invokes the delegates.

清单 13.6所示,注册结果相当冗长。然而,借助两个自写的帮助方法,您可以大大简化注册:

As listing 13.6 shows, the resulting registration is rather verbose. With the aid of two self-written helper methods, however, you can simplify the registration considerably:

builder.RegisterType<ThreeCourseMeal>().As<IMeal>()
    .WithParameter(Named("entrée"), InjectWith<ICourse>("entrée"))
    .WithParameter(Named("mainCourse"), InjectWith<ICourse>("mainCourse"))
    .WithParameter(Named("dessert"), InjectWith<ICourse>("dessert"));

通过引入NamedInjectWith<T>辅助方法,您简化了注册,减少了它的冗长,同时,更容易阅读正在发生的事情。它几乎开始读起来像诗歌(或一瓶陈年葡萄酒):

By introducing the Named and InjectWith<T> helper methods, you simplified the registration, reduced its verbosity, and at the same time, made it easier to read what’s going on. It almost starts to read like poetry (or a well-aged bottle of wine):

ThreeCourseMeal使用参数Namedentrée创建InjectedWith一个ICourse名为entrée的thy 。

create thy ThreeCourseMeal, with a parameter Namedentrée, InjectedWith an ICourse named entrée.

以下代码显示了这两种新方法:

The following code shows the two new methods:

Func<ParameterInfo, IComponentContext, bool> Named(string name)
{
    return (p, c) => p.Name == name;
}

Func<ParameterInfo, IComponentContext, object> InjectWith<T>(string name)
{
    return (p, c) => c.ResolveNamed<T>(name);
}

调用时,这两种方法都会创建一个新的委托来包装所提供的name参数。有时,除了为每个构造函数参数使用该方法之外别无他WithParameter法,但在其他情况下,您可以利用约定。

When called, both methods create a new delegate that wraps the supplied name argument. Sometimes there’s no other way than to use the WithParameter method for each and every constructor parameter, but in other cases, you can take advantage of conventions.

按约定解析命名组件

Resolving named components by convention

如果仔细检查清单 13.6,您会注意到一个重复的模式。每次调用WithParameter都只处理一个构造函数参数,但每次调用都valueAccessor做同样的事情:它使用IComponentContext来解析ICourse与参数同名的组件。

If you examine listing 13.6 closely, you’ll notice a repetitive pattern. Each call to WithParameter addresses only a single constructor parameter, but each valueAccessor does the same thing: it uses the IComponentContext to resolve an ICourse component with the same name as the parameter.

没有要求必须在构造函数参数之后命名组件,但在这种情况下,您可以利用此约定并以更简单的方式重写清单 13.6。以下清单演示了如何操作。

There’s no requirement that says you must name the component after the constructor parameter, but when this is the case, you can take advantage of this convention and rewrite listing 13.6 in a simpler way. The following listing demonstrates how.

清单 13.7用约定 覆盖自动装配

Listing 13.7 Overriding Auto-Wiring with a convention

builder.RegisterType<ThreeCourseMeal>().As<IMeal>()
    .WithParameter(
        (p, c) => true,
        (p, c) => c.ResolveNamed(p.Name, p.ParameterType));

这可能有点令人惊讶,但您可以ThreeCourseMeal通过同一个WithParameter调用来处理该类的所有三个构造函数参数。您通过声明此实例将处理 Autofac 可能抛给它的任何参数来做到这一点。因为你只使用这个方法来配置ThreeCourseMeal类,所以约定只适用于这个有限的范围。

It might be a little surprising, but you can address all three constructor parameters of the ThreeCourseMeal class with the same WithParameter call. You do that by stating that this instance will handle any parameter Autofac might throw at it. Because you only use this method to configure the ThreeCourseMeal class, the convention only applies within this limited scope.

由于谓词始终返回true,因此将为所有三个构造函数参数调用第二个代码块。在这三种情况下,它都会要求IComponentContext解析与参数具有相同名称和类型的组件。这在功能上与您在清单 13.6中所做的相同。

As the predicate always returns true, the second code block will be invoked for all three constructor parameters. In all three cases, it’ll ask IComponentContext to resolve a component that has the same name and type as the parameter. This is functionally the same as what you did in listing 13.6.

清单 13.6一样,您可以创建清单 13.7的简化版本。但我们会将其作为练习留给读者。

As in listing 13.6, you can create a simplified version of listing 13.7. But we’ll leave this as an exercise for the reader.

覆盖自动装配通过将参数显式映射到命名组件是一种普遍适用的解决方案。即使您在Composition Root的一部分中配置命名组件而在完全不同的部分中配置消费者,您也可以这样做,因为将命名组件与参数联系在一起的唯一标识是名称。这始终是可能的,但如果您要管理许多名称,则可能会很脆弱。当提示您使用命名组件的最初原因是为了处理歧义时,更好的解决方案是设计您自己的 API 来消除这种歧义。它通常会带来更好的整体设计。

Overriding Auto-Wiring by explicitly mapping parameters to named components is a universally applicable solution. You can do this even if you configure the named components in one part of the Composition Root and the consumer in a completely different part, because the only identification that ties a named component together with a parameter is the name. This is always possible but can be brittle if you have many names to manage. When the original reason prompting you to use named components is to deal with ambiguity, a better solution is to design your own API to get rid of that ambiguity. It often leads to a better overall design.

在下一节中,您将看到如何使用更明确和更灵活的方法,在这种方法中您允许在一餐中加入任意数量的课程。为此,您必须了解 Autofac 如何处理列表和序列。

In the next section, you’ll see how to use the less ambiguous and more flexible approach, where you allow any number of courses in a meal. To this end, you must learn how Autofac deals with lists and sequences.

13.4.2 接线顺序

13.4.2 Wiring sequences

在 6.1.1 节中,我们讨论了构造函数注入如何作为单一职责原则违规的警告系统。当时的教训是,与其将构造函数过度注入视为构造函数注入模式的弱点,不如庆幸它使有问题的设计如此明显。

In section 6.1.1, we discussed how Constructor Injection acts as a warning system for Single Responsibility Principle violations. The lesson then was that instead of viewing Constructor Over-Injection as a weakness of the Constructor Injection pattern, you should rather rejoice that it makes problematic design so obvious.

当谈到DI 容器和歧义时,我们看到了类似的关系。DI 容器通常不会以优雅的方式处理歧义。尽管您可以制作像 Autofac 这样的优秀DI 容器来处理它,但它看起来很尴尬。这通常表明您可以改进代码的设计。

When it comes to DI Containers and ambiguity, we see a similar relationship. DI Containers generally don’t deal with ambiguity in a graceful manner. Although you can make a good DI Container like Autofac deal with it, it can seem awkward. This is often an indication that you could improve on the design of your code.

不要感到受 Autofac 的束缚,您应该接受它的约定,让它引导您实现更好、更一致的设计。在本节中,我们将查看一个示例,演示如何通过重构消除歧义,并展示 Autofac 如何处理序列、数组和列表。

Instead of feeling constrained by Autofac, you should embrace its conventions and let it guide you toward a better and more consistent design. In this section, we’ll look at an example that demonstrates how you can refactor away from ambiguity, as well as show how Autofac deals with sequences, arrays, and lists.

通过消除歧义重构更好的课程

Refactoring to a better course by removing ambiguity

在 13.4.1 节中,您看到了 theThreeCourseMeal及其固有的歧义如何迫使您放弃自动装配而改用WithParameter. 这应该会促使您重新考虑 API 设计。例如,一个简单的概括走向它的实现IMeal采用任意数量的ICourse实例,而不是恰好三个实例,就像ThreeCourseMeal类的情况一样:

In section 13.4.1, you saw how the ThreeCourseMeal and its inherent ambiguity forced you to abandon Auto-Wiring and instead use WithParameter. This should prompt you to reconsider the API design. For example, a simple generalization moves toward an implementation of IMeal that takes an arbitrary number of ICourse instances, instead of exactly three, as was the case with the ThreeCourseMeal class:

public Meal(IEnumerable<ICourse> courses)

请注意,ICourse构造函数中不需要三个不同的实例,对一个实例的单一依赖IEnumerable<ICourse>使您可以为班级提供任意数量的课程Meal——从零到……很多!这解决了含糊不清的问题,因为现在只有一个Dependency。此外,它还通过提供一个单一的通用类来改进 API 和实现,该类可以模拟不同类型的膳食,从具有单一课程的简单膳食到精心制作的 12 道菜晚餐。

Notice that instead of requiring three distinct ICourse instances in the constructor, the single dependency on an IEnumerable<ICourse> instance lets you provide any number of courses to the Meal class — from zero to ... a lot! This solves the issue with ambiguity, because there’s now only a single Dependency. In addition, it also improves the API and implementation by providing a single, general-purpose class that can model different types of meals, from a simple meal with a single course to an elaborate 12-course dinner.

在本节中,我们将了解如何配置 Autofac 以Meal使用适当的ICourse Dependencies连接实例。完成后,您应该对需要使用Dependencies序列配置实例时可用的选项有一个很好的了解。

In this section, we’ll look at how you can configure Autofac to wire up Meal instances with appropriate ICourse Dependencies. When you’re done, you should have a good idea of the options available when you need to configure instances with sequences of Dependencies.

自动接线序列

Auto-Wiring sequences

Autofac 对序列有很好的理解,所以如果你想使用给定服务的所有注册组件,自动装配就可以了。例如,您可以IMeal像这样配置服务:

Autofac has a good understanding of sequences, so if you want to use all registered components of a given service, Auto-Wiring just works. As an example, you can configure the IMeal service like this:

builder.RegisterType<Rillettes>().As<IIngredient>();
builder.RegisterType<CordonBlue>().As<IIngredient>();
builder.RegisterType<MousseAuChocolat>().As<IIngredient>();

builder.RegisterType<Meal>().As<IMeal>();

请注意,这是从具体类型到抽象的完全标准映射。Autofac 自动理解Meal构造函数并确定正确的操作过程是解析所有ICourse组件。当您解析 时IMeal,您将获得一个包含以下组件的Meal实例: 、和。ICourseRillettesCordonBleuMousseAuChocolat

Notice that this is a completely standard mapping from a concrete type to an Abstraction. Autofac automatically understands the Meal constructor and determines that the correct course of action is to resolve all ICourse components. When you resolve IMeal, you get a Meal instance with the ICourse components: Rillettes, CordonBleu, and MousseAuChocolat.

Autofac 自动处理序列,除非您另外指定,否则它会执行您期望的操作:它将一系列依赖项解析为该类型的所有已注册组件。只有当您需要明确地从更大的集合中挑选一些组件时,您才需要做更多的事情。让我们看看如何做到这一点。

Autofac automatically handles sequences, and, unless you specify otherwise, it does what you’d expect it to do: it resolves a sequence of Dependencies to all registered components of that type. Only when you need to explicitly pick some components from a larger set do you need to do more. Let’s see how you can do that.

从更大的集合中只挑选一些组件

Picking only some components from a larger set

Autofac 注入所有组件的默认策略通常是正确的策略,但如图 13.4所示,在某些情况下,您可能只想从所有已注册组件的较大集合中挑选一些已注册组件。

Autofac’s default strategy of injecting all components is often the correct policy, but as figure 13.4 shows, there may be cases where you want to pick only some registered components from the larger set of all registered components.

13-04.eps

图 13.4 从更大的所有已注册组件集合中挑选组件

Figure 13.4 Picking components from a larger set of all registered components

当你之前让Autofac Auto-Wire所有配置的实例时,就对应了图中右侧描述的情况。如果你想注册一个组件,如左侧所示,你必须明确定义应该使用哪些组件。为了实现这一点,您可以使用该WithParameter方法就像您在清单 13.6 和 13.7 中所做的那样。这一次,您要处理的Meal是仅采用单个参数的构造函数。下面的清单演示了如何实现的值提供部分WithParameter以从IComponentContext.

When you previously let Autofac Auto-Wire all configured instances, it corresponded to the situation depicted on the right side of the figure. If you want to register a component as shown on the left side, you must explicitly define which components should be used. In order to achieve this, you can use the WithParameter method the way you did in listings 13.6 and 13.7. This time, you’re dealing with the Meal constructor that only takes a single parameter. The following listing demonstrates how you can implement the value-providing part of WithParameter to explicitly pick named components from the IComponentContext.

清单 13.8 将命名组件注入序列

Listing 13.8 Injecting named components into a sequence

builder.RegisterType<Meal>().As<IMeal>()
    .WithParameter(
        (p, c) => true,
        (p, c) => new[]
        {
            c.ResolveNamed<ICourse>("entrée"),
            c.ResolveNamed<ICourse>("mainCourse"),
            c.ResolveNamed<ICourse>("dessert")
        });

正如您在 13.4.1 节中看到的,该WithParameter方法将两个委托作为输入参数。第一个是用于确定是否应调用第二个委托的谓词。在这种情况下,您决定偷懒一点并返回true。您知道Meal该类只有一个构造函数参数,所以这会起作用。但是,如果您稍后重构Meal该类以采用第二个构造函数参数,这可能无法再正常工作。为参数类型定义显式检查可能更安全。

As you saw in section 13.4.1, the WithParameter method takes two delegates as input parameters. The first is a predicate that’s used to determine if the second delegate should be invoked. In this case, you decide to be a bit lazy and return true. You know that the Meal class has only a single constructor parameter, so this’ll work. But if you later refactor the Meal class to take a second constructor parameter, this may not work correctly anymore. It might be safer to define an explicit check for the parameter type.

第二个委托提供参数的值。您用于将三个命名组件解析为一个数组。结果是一个实例数组,它与.IComponentContextICourseIEnumerable<ICourse>

The second delegate provides the value for the parameter. You use IComponentContext to resolve three named components into an array. The result is an array of ICourse instances, which is compatible with IEnumerable<ICourse>.

Autofac 本身就理解序列;除非您需要从给定类型的所有服务中明确地只选择一些组件,否则 Autofac 会自动做正确的事情。Auto-Wiring不仅适用于单个实例,也适用于序列,容器将一个序列映射到相应类型的所有已配置实例。具有相同抽象的多个实例的一种可能不太直观的用法是装饰器设计模式,我们将在接下来讨论。

Autofac natively understands sequences; unless you need to explicitly pick only some components from all services of a given type, Autofac automatically does the right thing. Auto-Wiring works not only with single instances, but also for sequences, and the container maps a sequence to all configured instances of the corresponding type. A perhaps less intuitive use of having multiple instances of the same Abstraction is the Decorators design pattern, which we’ll discuss next.

13.4.3 接线装饰器

13.4.3 Wiring Decorators

在 9.1.1 节中,我们讨论了装饰器设计模式在实现横切关注点时如何发挥作用。根据定义,装饰器引入了相同抽象的多种类型。至少,您有两个抽象实现:装饰器本身和装饰类型。如果你堆叠装饰器,你可以拥有更多。这是对同一服务进行多次注册的另一个示例。与前面的部分不同,这些注册在概念上不是相等的,而是彼此的依赖关系。

In section 9.1.1, we discussed how the Decorator design pattern is useful when implementing Cross-Cutting Concerns. By definition, Decorators introduce multiple types of the same Abstraction. At the very least, you have two implementations of an Abstraction: the Decorator itself and the decorated type. If you stack the Decorators, you can have even more. This is another example of having multiple registrations of the same service. Unlike the previous sections, these registrations aren’t conceptually equal but rather Dependencies of each other.

在 Autofac 中应用装饰器有多种策略,例如使用前面讨论的WithParameter或使用代码块,正如我们在 13.3.2 节中讨论的那样。然而,在本节中,我们将重点关注和方法的使用,因为它们使配置装饰器变得轻而易举。RegisterDecoratorRegisterGenericDecorator

There are multiple strategies for applying Decorators in Autofac, such as using the previously discussed WithParameter or using code blocks, as we discussed in section 13.3.2. In this section, however, we’ll focus on the use of the RegisterDecorator and RegisterGenericDecorator methods because they make configuring Decorators a no-brainer.

装饰非泛型抽象RegisterDecorator

Decorating non-generic Abstractions with RegisterDecorator

Autofac 通过方法内置了对装饰器的支持RegisterDecorator。以下示例显示如何使用此方法应用于Breadinga VealCutlet

Autofac has built-in support for Decorators via the RegisterDecorator method. The following example shows how to use this method to apply Breading to a VealCutlet:

builder.RegisterType<VealCutlet>()    ①  
    .As<IIngredient>();    ①  

builder.RegisterDecorator<Breading, IIngredient>();  ②  

正如你在第 9 章中学到的,当你在小牛肉排上切开一个口袋,然后在给小牛肉排裹上面包屑之前,将火腿、奶酪和大蒜放入口袋中,你就会得到 Cordon Bleu。下面的例子展示了如何在Decorator和DecoratorHamCheeseGarlic之间添加一个Decorator:VealCutletBreading

As you learned in chapter 9, you get Cordon Bleu when you slit open a pocket in the veal cutlet and add ham, cheese, and garlic into the pocket before breading the cutlet. The following example shows how to add a HamCheeseGarlic Decorator in between VealCutlet and the Breading Decorator:

builder.RegisterType<VealCutlet>()
    .As<IIngredient>();

builder.RegisterDecorator<HamCheeseGarlic,    ①  
    IIngredient>();    ①  

builder.RegisterDecorator<Breading, IIngredient>();

通过在注册之前放置这个新注册BreadingHamCheeseGarlic装饰器首先被包装。这导致对象图等于以下纯 DI版本:

By placing this new registration before the Breading registration, the HamCheeseGarlic Decorator is wrapped first. This results in an object graph equal to the following Pure DI version:

new Breading(    ①  
    new HamCheeseGarlic(    ①  
        new VealCutlet()));    ①  

使用该RegisterDecorator方法链接装饰器在 Autofac 中很容易。同样,您可以应用通用装饰器,如下所示。

Chaining Decorators using the RegisterDecorator method is easy in Autofac. Likewise, you can apply generic Decorators, as you’ll see next.

装饰通用抽象RegisterGenericDecorator

Decorating generic Abstractions with RegisterGenericDecorator

在第 10 章中,我们定义了多个可以应用于任何ICommandService<TCommand>实现的通用装饰器。在本章的剩余部分,我们将把成分和课程放在一边,看看如何使用 Autofac 注册这些通用装饰器。以下清单演示了如何ICommandService<TCommand>使用 10.3 节中介绍的三个装饰器注册所有实现。

During the course of chapter 10, we defined multiple generic Decorators that could be applied to any ICommandService<TCommand> implementation. In the remainder of this chapter, we’ll set our ingredients and courses aside, and take a look at how to register these generic Decorators using Autofac. The following listing demonstrates how to register all ICommandService<TCommand> implementations with the three Decorators presented in section 10.3.

清单 13.9 装饰通用的自动注册抽象

Listing 13.9 Decorating generic Auto-Registered Abstractions

builder.RegisterAssemblyTypes(assembly)
    .AsClosedTypesOf(typeof(ICommandService<>));

builder.RegisterGenericDecorator(
    typeof(AuditingCommandServiceDecorator<>),
    typeof(ICommandService<>));

builder.RegisterGenericDecorator(
    typeof(TransactionCommandServiceDecorator<>),
    typeof(ICommandService<>));

builder.RegisterGenericDecorator(
    typeof(SecureCommandServiceDecorator<>),
    typeof(ICommandService<>));
13-05.eps

图 13.5 用事务、审计和安全方面丰富一个真正的命令服务

Figure 13.5 Enriching a real command service with transaction, auditing, and security aspects

正如您在清单 13.3中看到的,清单 13.9用于注册任意实现。然而,为了注册通用装饰器,Autofac 提供了一种不同的方法—— . 清单 13.9的配置结果是图 13.5,我们之前在 10.3.4 节中讨论过。RegisterAssemblyTypesICommandService<TCommand>RegisterGenericDecorator

As you saw in listing 13.3, listing 13.9 uses RegisterAssemblyTypes to register arbitrary ICommandService<TCommand> implementations. To register generic Decorators, however, Autofac provides a different method — RegisterGenericDecorator. The result of the configuration of listing 13.9 is figure 13.5, which we discussed previously in section 10.3.4.

您可以通过不同的方式配置装饰器,但在本节中,我们重点介绍了专门为此任务设计的 Autofac 方法。Autofac 允许您以几种不同的方式处理多个实例:您可以将组件注册为彼此的替代品,因为对等体解析为序列,或作为分层装饰器。在许多情况下,Autofac 会弄清楚要做什么,但如果您需要更明确的控制,您始终可以明确定义服务的组成方式。

You can configure Decorators in different ways, but in this section, we focused on Autofac’s methods that were explicitly designed for this task. Autofac lets you work with multiple instances in several different ways: you can register components as alternatives to each other, as peers resolved as sequences, or as hierarchical Decorators. In many cases, Autofac figures out what to do, but you can always explicitly define how services are composed if you need more-explicit control.

尽管依赖依赖序列的消费者可以最直观地使用同一抽象的多个实例,但装饰器是另一个很好的例子。但是还有第三种可能有点令人惊讶的情况,其中多个实例开始发挥作用,这就是复合设计模式。

Although consumers that rely on sequences of Dependencies can be the most intuitive use of multiple instances of the same Abstraction, Decorators are another good example. But there’s a third and perhaps a bit surprising case where multiple instances come into play, which is the Composite design pattern.

13.4.4 布线复合材料

13.4.4 Wiring Composites

在本书的学习过程中,我们多次讨论了复合设计模式。例如,在 6.1.2 节中,您创建了一个(清单 6.4),它实现并包装了一系列实现。CompositeNotificationServiceINotificationServiceINotificationService

During the course of this book, we discussed the Composite design pattern on several occasions. In section 6.1.2, for instance, you created a CompositeNotificationService (listing 6.4) that both implemented INotificationService and wrapped a sequence of INotificationService implementations.

布线非通用复合材料

Wiring non-generic Composites

让我们来看看如何CompositeNotificationService在 Autofac 中像第 6 章那样注册 Composites。下面的清单再次显示了这个类。

Let’s take a look at how you can register Composites like the CompositeNotificationService from chapter 6 in Autofac. The following listing shows this class again.

清单 13.10来自第 6 章 的CompositeCompositeNotificationService

Listing 13.10 The CompositeNotificationService Composite from chapter 6

public class CompositeNotificationService : INotificationService
{
    private readonly IEnumerable<INotificationService> services;

    public CompositeNotificationService(
        IEnumerable<INotificationService> services)
    {
        this.services = services;
    }

    public void OrderApproved(Order order)
    {
        foreach (INotificationService service in this.services)
        {
            service.OrderApproved(order);
        }
    }
}

注册 Composite 需要将其添加为默认注册,同时向其注入一系列命名实例:

Registering a Composite requires that it be added as a default registration while injecting it with a sequence of named instances:

builder.RegisterType<OrderApprovedReceiptSender>()
    .Named<INotificationService>("service");
builder.RegisterType<AccountingNotifier>()
    .Named<INotificationService>("service");
builder.RegisterType<OrderFulfillment>()
    .Named<INotificationService>("service");

builder.Register(c =>
    new CompositeNotificationService(
        c.ResolveNamed<IEnumerable<INotificationService>>("service")))
    .As<INotificationService>();

在这里,使用Autofac的自动INotificationService装配 API以相同的名称service注册了三个实现。另一方面,是使用委托注册的。在委托内部,手动更新 Composite 并注入一个. 通过指定服务名称,解析之前的命名注册。CompositeNotificationServiceIEnumerable<INotificationService>

Here, three INotificationService implementations are registered by the same name, service, using the Auto-Wiring API of Autofac. The CompositeNotificationService, on the other hand, is registered using a delegate. Inside the delegate, the Composite is newed up manually and injected with an IEnumerable<INotificationService>. By specifying the service name, the previous named registrations are resolved.

由于通知服务的数量可能会随着时间的推移而增加,您可以通过应用自动注册来减轻组合根的负担。使用该方法,您可以在一个简单的一行中转换以前的注册列表。RegisterAssemblyTypes

Because the number of notification services will likely grow over time, you can reduce the burden on your Composition Root by applying Auto-Registration. Using the RegisterAssemblyTypes method, you can turn the previous list of registrations in a simple one-liner.

清单 13.11 注册CompositeNotificationService

Listing 13.11 Registering CompositeNotificationService

builder.RegisterAssemblyTypes(assembly)
    .Named<INotificationService>("service");

builder.Register(c =>
    new CompositeNotificationService(
        c.ResolveNamed<IEnumerable<INotificationService>>("service")))
    .As<INotificationService>();

这看起来相当简单,但看起来是骗人的。将注册任何实现. 当您尝试运行前面的代码时,根据您的 Composite 所在的程序集,Autofac 可能会抛出以下异常:RegisterAssemblyTypesINotificationService

This looks reasonably simple, but looks are deceiving. RegisterAssemblyTypes will register any non-generic implementation that implements INotificationService. When you try to run the previous code, depending on which assembly your Composite is located in, Autofac might throw the following exception:

检测到循环组件依赖性:CompositeNotificationService -> INotificationService[] -> CompositeNotificationService -> INotificationService[] -> CompositeNotificationService。

Circular component dependency detected: CompositeNotificationService -> INotificationService[] -> CompositeNotificationService -> INotificationService[] -> CompositeNotificationService.

Autofac 检测到循环依赖。(我们在 6.3 节中详细讨论了依赖循环。)幸运的是,它的异常消息非常清楚。它描述了CompositeNotificationService依赖于INotificationService[]CompositeNotificationService包装了一系列,但该INotificationService序列本身又包含CompositeNotificationService。这意味着这CompositeNotificationService是注入到CompositeNotificationService. 这是一个不可能构建的对象图。

Autofac detected a cyclic Dependency. (We discussed Dependency cycles in detail in section 6.3.) Fortunately, its exception message is pretty clear. It describes that CompositeNotificationService depends on INotificationService[]. The CompositeNotificationService wraps a sequence of INotificationService, but that sequence itself again contains CompositeNotificationService. What this means is that CompositeNotificationService is an element of the sequence that’s injected into CompositeNotificationService. This is an object graph that’s impossible to construct.

CompositeNotificationService成为序列的一部分,因为 AutofacRegisterAssemblyTypes注册了它找到的所有非泛型INotificationService实现。在这种情况下,CompositeNotificationService被放置在与所有其他实现相同的程序集中。

CompositeNotificationService became a part of the sequence because Autofac’s RegisterAssemblyTypes registers all non-generic INotificationService implementations it finds. In this case, CompositeNotificationService was placed in the same assembly as all other implementations.

有多种解决方法。最简单的解决方案是将 Composite 移动到不同的程序集;例如,包含Composition Root的程序集。这会阻止RegisterAssemblyTypes选择类型,因为它是随特定Assembly实例一起提供的。另一种选择是过滤CompositeNotificationService 不在列表中。一种优雅的方法是使用以下Except方法:

There are multiple ways around this. The simplest solution is to move the Composite to a different assembly; for instance, the assembly containing the Composition Root. This prevents RegisterAssemblyTypes from selecting the type, because it’s provided with a particular Assembly instance. Another option is to filter the CompositeNotificationService out of the list. An elegant way of doing this is using the Except method:

builder.RegisterAssemblyTypes(assembly)
    .Except<CompositeNotificationService>()
    .Named<INotificationService>("service");

然而,复合类并不是唯一可能需要删除的类。您必须对任何 Decorator 执行相同的操作。这并不是特别困难,但是因为通常会有更多的 Decorator 实现,所以您最好查询类型信息以查明该类型是否表示 Decorator。以下示例显示了如何使用自定义帮助器方法也可以过滤掉装饰器:IsDecoratorFor

Composite classes, however, aren’t the only classes that might require removal. You’ll have to do the same for any Decorator. This isn’t particularly difficult, but because there’ll typically be more Decorator implementations, you might be better off querying the type information to find out whether the type represents a Decorator or not. The following example shows how you can filter out Decorators as well, using a custom IsDecoratorFor helper method:

builder.RegisterAssemblyTypes(assembly)
    .Except<CompositeNotificationService>()
    .Where(type => !IsDecoratorFor<INotificationService>(type))
    .Named<INotificationService>("service");

以下示例显示了该IsDecoratorFor方法:

And the following example shows the IsDecoratorFor method:

private static bool IsDecoratorFor<T>(Type type)
{
    return typeof(T).IsAssignableFrom(type) &&
        type.GetConstructors()[0].GetParameters()
            .Any(p => p.ParameterType == typeof(T));
}

IsDecoratorFor方法期望一个类型具有单个构造函数。当一个类型既实现了给定的T 抽象并且它的构造函数也需要一个T.

The IsDecoratorFor method expects a type to have a single constructor. A type is considered to be a Decorator when it both implements the given T Abstraction and its constructor also requires a T.

布线通用复合材料

Wiring generic Composites

在 13.4.3 节中,您看到了如何使用 Autofac 的RegisterGenericDecorator方法使注册通用装饰器成为儿童游戏。在本节中,我们将看看如何为通用抽象注册 Composites 。

In section 13.4.3, you saw how using Autofac’s RegisterGenericDecorator method made registering generic Decorators child’s play. In this section, we’ll take a look at how you can register Composites for generic Abstractions.

在 6.1.3 节中,您指定了CompositeEventHandler<TEvent>(清单 6.12)作为一系列实现的复合IEventHandler<TEvent>实现。让我们看看您是否可以使用其包装的事件处理程序实现来注册 Composite。

In section 6.1.3, you specified the CompositeEventHandler<TEvent> class (listing 6.12) as a Composite implementation over a sequence of IEventHandler<TEvent> implementations. Let’s see if you can register the Composite with its wrapped event handler implementations.

让我们从事件处理程序的自动注册开始. 正如您之前所见,这是使用以下RegisterAssemblyTypes方法完成的:

Let’s start with Auto-Registration of the event handlers. As you’ve seen previously, this is done using the RegisterAssemblyTypes method:

builder.RegisterAssemblyTypes(assembly)
    .As(type =>
        from interfaceType in type.GetInterfaces()
        where interfaceType.IsClosedTypeOf(typeof(IEventHandler<>))
        select new KeyedService("handler", interfaceType));

此示例使用As允许提供实例序列的重载。A类Autofac.Core.KeyedServiceKeyedService是结合了密钥和服务类型的小型数据对象。

This example makes use of the As overload that allows supplying a sequence of Autofac.Core.KeyedService instances. A KeyedService class is a small data object that combines both a key and a service type.

Autofac 通过该As方法运行它在程序集中找到的任何类型。您可以使用 LINQ 查询来查找类型的已实现接口,该接口是IEventHandler<TEvent>. 对于程序集中的大多数类型,此查询不会产生任何结果,因为大多数类型不实现IEventHandler<TEvent>. 对于这些类型,不会向ContainerBuilder.

Autofac runs any type it finds in the assembly through the As method. You can use a LINQ query to find the type’s implemented interface that’s a closed-generic version of IEventHandler<TEvent>. For most types in the assembly, this query won’t yield any results, because most types don’t implement IEventHandler<TEvent>. For those types, no registration is added to ContainerBuilder.

尽管这非常复杂,但不必过滤掉通用的复合材料和装饰器。RegisterAssemblyTypes只选择非通用实现。通用类型(例如CompositeEventHandler<TEvent>)不会导致任何问题,并且不必过滤掉或移动到其他程序集。这是幸运的,因为编写一个IsDecoratorFor可以处理通用抽象的版本一点也不有趣。

Even though this is quite complex, generic Composites and Decorators don’t have to be filtered out. RegisterAssemblyTypes only selects non-generic implementations. Generic types, such as CompositeEventHandler<TEvent>, won’t cause any problem, and don’t have to be filtered out or moved to a different assembly. This is fortunate, because it wouldn’t be fun at all to have to write a version of IsDecoratorFor that could handle generic Abstractions.

剩下的就是注册CompositeEventHandler<TEvent>. 因为此类型是通用的,所以您不能使用Register接受谓词的重载。相反,您使用RegisterGeneric. 此方法允许在通用实现与其抽象之间建立映射,类似于您在 中看到的那样RegisterGenericDecorator。要获得要注入到 Composite 的构造函数参数中的命名注册序列,您可以再次使用通用WithParameter方法:

What remains is the registration for CompositeEventHandler<TEvent>. Because this type is generic, you can’t use the Register overload that takes in a predicate. Instead, you use RegisterGeneric. This method allows making a mapping between a generic implementation and its Abstraction, similar to what you saw with RegisterGenericDecorator. To get the sequence of named registrations to be injected into the Composite’s constructor argument, you can once more use the versatile WithParameter method:

builder.RegisterGeneric(typeof(CompositeEventHandler<>))
    .As(typeof(IEventHandler<>))
    .WithParameter(
        (p, c) => true,
        (p, c) => c.ResolveNamed("handler", p.ParameterType));

因为CompositeEventHandler<TEvent>包含单个构造函数参数,所以您可以通过让谓词返回来简化注册以应用于所有参数true

Because CompositeEventHandler<TEvent> contains a single constructor parameter, you simplify the registration to apply to all parameters by letting the predicate return true.

当请求WithParameter关闭时调用委托。IEventHandler<TEvent>因此,在调用的时候,可以通过调用来获取构造函数参数的类型p.ParameterType。例如,如果IEventHandler<OrderApproved>请求 an,则参数类型将为IEnumerable<IEventHandler<OrderApproved>>. 通过将此类型传递给ResolveNamed具有序列名称处理程序的方法,Autofac 解析先前注册的实现的命名实例序列IEventHandler<OrderApproved>

The WithParameter delegates are called when a closed IEventHandler<TEvent> is requested. Therefore, at the time of invocation, you can get the type of the constructor parameter by calling p.ParameterType. For example, if an IEventHandler<OrderApproved> is requested, the parameter type will be IEnumerable<IEventHandler<OrderApproved>>. By passing this type on to the ResolveNamed method with the sequence name handler, Autofac resolves the previously registered sequence of named instances that implement IEventHandler<OrderApproved>.

尽管装饰器的注册很简单,但不幸的是,这不适用于合成器。Autofac 在设计时还没有考虑到 Composite 设计模式。这很可能会在未来的版本中改变。

Although the registration of Decorators is simple, this unfortunately doesn’t hold for Composites. Autofac hasn’t been designed — yet — with the Composite design pattern in mind. It’s likely this will change in a future version.

我们对 Autofac DI 容器的讨论到此结束。在下一章中,我们将把注意力转向 Simple Injector。

This completes our discussion of the Autofac DI Container. In the next chapter, we’ll turn our attention to Simple Injector.

概括

Summary

  • Autofac DI Container提供了相当全面的 API 并解决了您在使用DI Containers时通常遇到的许多棘手情况。
  • The Autofac DI Container offers a fairly comprehensive API and addresses many of the trickier situations you typically encounter when you use DI Containers.
  • Autofac 的一个重要的总体主题似乎是明确性。它不会试图猜测您的意思,而是提供一个易于使用的 API,为您提供显式启用功能的选项。
  • An important overall theme for Autofac seems to be one of explicitness. It doesn’t attempt to guess what you mean, but rather offers an easy-to-use API that provides you with options to explicitly enable features.
  • Autofac 在配置和使用容器之间强制执行更严格的关注点分离。您使用ContainerBuilder实例配置组件,但ContainerBuilder无法解析组件。完成配置ContainerBuilder后,您可以使用它来构建IContainer可用于解析组件的 。
  • Autofac enforces stricter separation of concerns between configuring and consuming a container. You configure components using a ContainerBuilder instance, but a ContainerBuilder can’t resolve components. When you’re done configuring a ContainerBuilder, you use it to build an IContainer that you can use to resolve components.
  • 使用 Autofac,直接从根容器解析是一种不好的做法。这很容易导致内存泄漏或并发错误。相反,您应该始终从生命周期范围内解决。
  • With Autofac, resolving from the root container directly is a bad practice. This can easily lead to memory leaks or concurrency bugs. Instead, you should always resolve from a lifetime scope.
  • Autofac 支持标准的LifestylesTransientSingletonScoped
  • Autofac supports the standard Lifestyles: Transient, Singleton, and Scoped.
  • Autofac 通过提供允许提供代码块的 API 允许使用不明确的构造函数和类型。这允许创建要执行的服务的任何代码。
  • Autofac allows working with ambiguous constructors and types by providing an API that allows supplying code blocks. This allows any code that creates a service to be executed.

14

简单注入器 DI 容器

14

The Simple Injector DI Container

在这一章当中

In this chapter

  • 使用 Simple Injector 的基本注册 API
  • Working with Simple Injector’s basic registration API
  • 管理组件生命周期
  • Managing component lifetime
  • 配置困难的 API
  • Configuring difficult APIs
  • 配置序列、装饰器和组合
  • Configuring sequences, Decorators, and Composites

在上一章中,我们了解了 Nicholas Blumhardt 在 2007 年创建的 Autofac DI Container。三年后,Steven 创建了 Simple Injector,我们将在本章中对其进行研究。我们将对 Simple Injector 进行与上一章对 Autofac 相同的处理。您将看到如何使用 Simple Injector 来应用第 1-3 部分中介绍的原则和模式。

In the previous chapter, we looked at the Autofac DI Container, created by Nicholas Blumhardt in 2007. Three years later, Steven created Simple Injector, which we’ll examine in this chapter. We’ll give Simple Injector the same treatment that we gave Autofac in the last chapter. You’ll see how you can use Simple Injector to apply the principles and patterns presented in parts 1–3.

本章分为四节。您可以独立阅读每个部分,尽管第一部分是其他部分的先决条件,而第四部分依赖于第三部分介绍的一些方法和类。您可以将本章与第 4 部分的其余章节分开阅读,专门了解 Simple Injector,或者您可以将其与其他章节一起阅读以比较DI 容器

This chapter is divided into four sections. You can read each section independently, though the first section is a prerequisite for the other sections, and the fourth section relies on some methods and classes introduced in the third section. You can read this chapter apart from the rest of the chapters in part 4, specifically to learn about Simple Injector, or you can read it together with the other chapters to compare DI Containers.

尽管本章并未完整介绍 Simple Injector 容器,但它提供了足够的信息让您可以开始使用它。本章包含有关如何处理使用 Simple Injector 时可能出现的最常见问题的信息。有关此容器的更多信息,请参阅位于https://simpleinjector.org的 Simple Injector 主页。

Although this chapter isn’t a complete treatment of the Simple Injector container, it gives enough information that you can start using it. This chapter includes information on how to deal with the most common questions that may come up as you use Simple Injector. For more information about this container, see the Simple Injector home page at https://simpleinjector.org.

14.1 简单注入器介绍

14.1 Introducing Simple Injector

在本节中,您将了解从何处获得 Simple Injector、您将获得什么以及如何开始使用它。我们还将查看常见的配置选项。表 14.1提供了开始时可能需要的基本信息。

In this section, you’ll learn where to get Simple Injector, what you get, and how to start using it. We’ll also look at common configuration options. Table 14.1 provides fundamental information that you’re likely to need to get started.

表 14.1 简单注入器一览
回答
我从哪里得到它?在 Visual Studio 中,您可以通过 NuGet 获取它。包名称是SimpleInjector
支持哪些平台?.NET 4.0 和 .NET Standard 1.0(.NET Core 1.0、Mono 4.6、Xamarin.iOS 10.0、Xamarin.Mac 3.0、Xamarin.Android 7.0、UWP 10.0、Windows 8.0、Windows Phone 8.1)。
它要多少钱?没有什么。它是开源的。
它是如何获得许可的?麻省理工执照
我在哪里可以得到帮助?没有保证支持,但您可能会在 https://simpleinjector.org/forum 的官方论坛上获得帮助,或者通过在https://stackoverflow.com/的 Stack Overflow 上提问来获得帮助。
本章基于哪个版本?4.4.3

在高层次上,使用 Simple Injector 与使用其他DI Containers没有什么不同。与 Autofac DI 容器(第 13 章介绍)和 Microsoft.Extensions.DependencyInjection DI 容器(第 15 章介绍)一样,使用过程分为两步,如图 14.1所示。

At a high level, using Simple Injector isn’t that different from using the other DI Containers. As with the Autofac DI Container (covered in chapter 13) and the Microsoft.Extensions.DependencyInjection DI Container (covered in chapter 15), usage is a two-step process, as figure 14.1 illustrates.

14-01.eps

图 14.1 使用 Simple Injector 的模式。首先,您配置一个Container,然后使用相同的容器实例从中解析组件。

Figure 14.1 The pattern for using Simple Injector. First, you configure a Container, and then, using the same container instance, you resolve components from it.

您可能还记得第 13 章,为了简化两步过程,Autofac 使用一个ContainerBuilder生成IContainer. 另一方面,Simple Injector 在同一个Container实例中集成了注册和解析。尽管如此,它仍然通过在解析第一个服务后不允许进行任何显式注册来强制注册成为一个两步过程。

As you might remember from chapter 13, to facilitate the two-step process, Autofac uses a ContainerBuilder class that produces an IContainer. Simple Injector, on the other hand, integrates both registration and resolution in the same Container instance. Still, it forces the registration to be a two-step process by disallowing any explicit registrations to be made after the first service is resolved.

尽管解决方案没有太大区别,但 Simple Injector 的注册 API 确实与大多数DI 容器的工作方式有很大不同。在其设计和实现中,它消除了许多常见错误原因的陷阱。我们已经讨论了整本书中的大部分陷阱,因此在本章中,我们将讨论 Simple Injector 和其他DI 容器之间的以下区别:

Although resolution isn’t that different, Simple Injector’s registration API does differ quite a lot from how most DI Containers work. In its design and implementation, it eliminates many pitfalls that are a common cause of bugs. We’ve discussed most of these pitfalls throughout the book, so in this chapter, we’ll discuss the following differences between Simple Injector and other DI Containers:

  • 范围是环境的,允许对象图始终从容器本身解析,以防止内存和并发错误。
  • Scopes are ambient, allowing object graphs to always be resolved from the container itself to prevent memory and concurrency bugs.
  • 序列通过不同的 API 注册,以防止意外的重复注册相互覆盖。
  • Sequences are registered through a different API to prevent accidental duplicate registrations from overriding each other.
  • 不能直接注册原始类型以防止注册变得不明确。
  • Primitive types can’t be registered directly to prevent registrations from becoming ambiguous.
  • 可以验证对象图以发现常见的配置错误,例如Captive Dependencies
  • Object graphs can be verified to spot common configuration errors, such as Captive Dependencies.

完成本节后,您应该对 Simple Injector 的整体使用模式有一个良好的感觉,并且您应该能够在行为良好的场景中开始使用它——所有组件都遵循适当的 DI 模式,例如构造函数注射。让我们从最简单的场景开始,看看如何使用 Simple Injector 容器解析对象。

When you’re done with this section, you should have a good feeling for the overall usage pattern of Simple Injector, and you should be able to start using it in well-behaved scenarios — where all components follow proper DI patterns, such as Constructor Injection. Let’s start with the simplest scenario and see how you can resolve objects using a Simple Injector container.

14.1.1 解析对象

14.1.1 Resolving objects

任何DI 容器的核心服务都是组合对象图。在本节中,我们将了解可让您使用简单注入器组合对象图的 API。

The core service of any DI Container is to compose object graphs. In this section, we’ll look at the API that lets you compose object graphs with Simple Injector.

如果您还记得关于使用 Autofac 解析组件的讨论,您可能还记得 Autofac 要求您先注册所有相关组件,然后才能解决它们。Simple Injector 不是这种情况;如果您请求具有无参数构造函数的具体类型,则无需配置。以下清单显示了 Simple Injector 最简单的可能用法之一。

If you remember the discussion about resolving components with Autofac, you may recall that Autofac requires you to register all relevant components before you can resolve them. This isn’t the case with Simple Injector; if you request a concrete type with a parameterless constructor, no configuration is necessary. The following listing shows one of the simplest possible uses of Simple Injector.

清单 14.1 Simple Injector 的最简单使用

Listing 14.1 Simplest possible use of Simple Injector

var container = new Container();    ①  

SauceBéarnaise sauce =
    container.GetInstance<SauceBéarnaise>();  ②  

给定一个实例,您可以使用通用方法SimpleInjector.ContainerGetInstance获取具体SauceBéarnaise类的实例。因为这个类有一个无参数的构造函数,Simple Injector 会自动创建它的一个实例。不需要显式配置容器。

Given an instance of SimpleInjector.Container, you can use the generic GetInstance method to get an instance of the concrete SauceBéarnaise class. Because this class has a parameterless constructor, Simple Injector automatically creates an instance of it. No explicit configuration of the container is necessary.

正如您在第 12.1.2 节中学到的,自动装配是通过使用类型信息自动组成对象图的能力。因为 Simple Injector 支持Auto-Wiring,所以即使在没有无参构造函数的情况下,只要涉及的构造函数参数都是具体类型,并且整个树中的所有参数都是带有无参构造函数的叶子类型,它也可以创建没有配置的实例。例如,考虑这个Mayonnaise构造函数:

As you learned in section 12.1.2, Auto-Wiring is the ability to automatically compose an object graph by making use of the type information. Because Simple Injector supports Auto-Wiring, even in the absence of a parameterless constructor, it can create instances without configurations as long as the involved constructor parameters are all concrete types, and all parameters in the entire tree have leaf types with parameterless constructors. As an example, consider this Mayonnaise constructor:

public Mayonnaise(EggYolk eggYolk, SunflowerOil oil)

尽管蛋黄酱配方有点简化,但假设EggYolkSunflowerOil都是具有无参数构造函数的具体类。虽然Mayonnaise它本身没有无参数构造函数,但 Simple Injector 会在没有任何配置的情况下创建它:

Although the mayonnaise recipe is a bit simplified, suppose both EggYolk and SunflowerOil are concrete classes with parameterless constructors. Although Mayonnaise itself has no parameterless constructor, Simple Injector creates it without any configuration:

var container = new Container();
Mayonnaise mayo = container.GetInstance<Mayonnaise>();

这是有效的,因为 Simple Injector 能够弄清楚如何创建所有必需的构造函数参数。但是一旦引入松散耦合,就必须通过将抽象映射到具体类型来配置简单注入器。

This works because Simple Injector is able to figure out how to create all required constructor parameters. But as soon as you introduce loose coupling, you must configure Simple Injector by mapping Abstractions to concrete types.

将抽象映射到具体类型

Mapping abstractions to concrete types

尽管 Simple Injector自动连接具体类型的能力有时会派上用场,但松散耦合需要您将抽象映射到具体类型。基于此类映射创建实例是任何DI Container提供的核心服务,但您仍然必须定义映射。在此示例中,您将IIngredient接口映射到具体SauceBéarnaise类,这使您可以成功解析IIngredient

Although Simple Injector’s ability to Auto-Wire concrete types certainly can come in handy from time to time, loose coupling requires you to map Abstractions to concrete types. Creating instances based on such maps is the core service offered by any DI Container, but you must still define the map. In this example, you map the IIngredient interface to the concrete SauceBéarnaise class, which allows you to successfully resolve IIngredient:

var container = new Container();

container.Register<IIngredient, SauceBéarnaise>();  ①  

IIngredient sauce =
    container.GetInstance<IIngredient>();    ②  

您使用该Container实例来注册类型和定义映射。在这里,泛型Register方法允许将抽象映射到特定的实现。这使您可以注册具体类型。由于之前的Register调用,SauceBéarnaise现在可以解析为IIngredient.

You use the Container instance to register types and define maps. Here, the generic Register method allows an Abstraction to be mapped to a particular implementation. This lets you register a concrete type. Because of the previous Register call, SauceBéarnaise can now be resolved as IIngredient.

在许多情况下,通用 API 就是您所需要的。不过,在某些情况下,您需要一种更弱类型的方式来解析服务。这也是可能的。

In many cases, the generic API is all you need. Still, there are situations where you need a more weakly typed way to resolve services. This is also possible.

解决弱类型服务

Resolving weakly typed services

有时您不能使用通用 API,因为您在设计时不知道合适的类型。您只有一个Type实例,但您仍然希望获得该类型的实例。您在第 7.3 节中看到了一个示例,其中我们讨论了 ASP.NET Core MVC 的IControllerActivator. 相关的方法是这个:

Sometimes you can’t use a generic API, because you don’t know the appropriate type at design time. All you have is a Type instance, but you’d still like to get an instance of that type. You saw an example of that in section 7.3, where we discussed ASP.NET Core MVC’s IControllerActivator class. The relevant method is this one:

object Create(ControllerContext context);

如前面清单 7.8 所示,ControllerContext捕获控制器的Type,您可以使用ControllerTypeInfo属性提取它ActionDescriptor财产的:

As shown previously in listing 7.8, the ControllerContext captures the controller’s Type, which you can extract using the ControllerTypeInfo property of the ActionDescriptor property:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();

因为你只有一个Type实例,你不能使用泛型GetInstance<T>方法,但必须求助于弱类型的 API。Simple Injector 提供了GetInstance方法的弱类型重载,使您可以Create像这样实现方法:

Because you only have a Type instance, you can’t use the generic GetInstance<T> method, but must resort to a weakly typed API. Simple Injector offers a weakly typed overload of the GetInstance method that lets you implement the Create method like this:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return container.GetInstance(controllerType);

的弱类型重载GetInstance允许您传递controllerType变量直接到 Simple Injector。通常,这意味着您必须将返回值转换为某种抽象,因为弱类型GetInstance方法会返回object. 但是,在 的情况下IControllerActivator,这不是必需的,因为 ASP.NET Core MVC 不需要控制器来实现任何接口或基类。

The weakly typed overload of GetInstance lets you pass the controllerType variable directly to Simple Injector. Typically, this means you have to cast the returned value to some Abstraction because the weakly typed GetInstance method returns object. In the case of IControllerActivator, however, this isn’t required, because ASP.NET Core MVC doesn’t require controllers to implement any interface or base class.

无论GetInstance您使用哪种重载,Simple Injector 都保证它会返回请求类型的实例或抛出异常(如果有)无法满足的依赖关系。当所有必需的依赖项都已正确配置后,Simple Injector 可以自动连接请求的类型。

No matter which overload of GetInstance you use, Simple Injector guarantees that it’ll return an instance of the requested type or throw an exception if there are Dependencies that can’t be satisfied. When all required Dependencies have been properly configured, Simple Injector can Auto-Wire the requested type.

为了能够解析请求的类型,所有松散耦合的依赖项必须已预先配置。您可以通过多种方式配置 Simple Injector;下一节回顾最常见的。

To be able to resolve the requested type, all loosely coupled Dependencies must have been previously configured. You can configure Simple Injector in many ways; the next section reviews the most common ones.

14.1.2 配置容器

14.1.2 Configuring the container

正如我们在 12.2 节中讨论的那样,您可以通过几种概念上不同的方式配置DI 容器。图 12.5 查看了选项:配置文件、配置即代码自动注册图 14.2再次显示了这些选项。

As we discussed in section 12.2, you can configure a DI Container in several conceptually different ways. Figure 12.5 reviewed the options: configuration files, Configuration as Code, and Auto-Registration. Figure 14.2 shows these options again.

14-02.eps

图 14.2针对显式维度和绑定程度显示了配置DI 容器 的最常用方法。

Figure 14.2 The most common ways to configure a DI Container shown against dimensions of explicitness and the degree of binding.

Simple Injector 的核心配置 API 以代码为中心,同时支持Configuration as Code和基于约定的Auto-Registration。基于文件的配置完全被排除在外。这不应该成为使用 Simple Injector 的障碍,因为正如我们在第 12 章中讨论的那样,通常应该避免使用这种配置方法。尽管如此,如果您的应用程序需要后期绑定,您自己添加基于文件的配置还是很容易的,我们将在本节后面讨论。

Simple Injector’s core configuration API is centered on code and supports both Configuration as Code and convention-based Auto-Registration. File-based configuration is left out completely. This shouldn’t be a obstacle to using Simple Injector because, as we discussed in chapter 12, this configuration method should generally be avoided. Still, if your application requires late binding, it’s quite easy to add a file-based configuration yourself, as we’ll discuss later in this section.

Simple Injector 允许您混合所有三种方法。在本节中,您将看到如何使用这三种类型的配置源中的每一种。

Simple Injector lets you mix all three approaches. In this section, you’ll see how to use each of these three types of configuration sources.

使用配置即代码配置容器

Configuring the container using Configuration as Code

在 14.1 节中,您简要了解了 Simple Injector 的强类型配置 API。在这里,我们将更详细地研究它。

In section 14.1, you saw a brief glimpse of Simple Injector’s strongly typed configuration API. Here, we’ll examine it in greater detail.

ContainerSimple Injector 中的所有配置都使用该类公开的 API. 最常用的Register方法之一是您已经看到的方法:

All configuration in Simple Injector uses the API exposed by the Container class. One of the most commonly used methods is the Register method that you’ve already seen:

container.Register<IIngredient, SauceBéarnaise>();

因为你想针对接口编程,所以你的大部分组件都将依赖于Abstractions。这意味着大多数组件将由其相应的Abstraction注册。当一个组件是对象图中最顶层的类型时,通过它的具体类型而不是它的抽象来解析它并不少见。例如,MVC 控制器由它们的具体类型解析。

Because you want to program to interfaces, most of your components will depend on Abstractions. This means that most components will be registered by their corresponding Abstraction. When a component is the topmost type in the object graph, its not uncommon to resolve it by its concrete type instead of its Abstraction. MVC controllers, for instance, are resolved by their concrete type.

通常,您可以通过抽象或具体类型注册一个类型,但不能同时注册。然而,这条规则也有例外。在 Simple Injector 中,通过具体类型和抽象来注册组件只是添加额外注册的问题:

In general, you would register a type either by its Abstraction or by its concrete type, but not both. There are exceptions to this rule, however. In Simple Injector, registering a component both by its concrete type and its Abstraction is simply a matter of adding an extra registration:

container.Register<IIngredient, SauceBéarnaise>();
container.Register<SauceBéarnaise>();

IIngredient您可以将类注册为它本身和它实现的接口,而不是仅将类注册为 。SauceBéarnaise这使容器能够解决对和的请求IIngredient

Instead of registering the class only as IIngredient, you can register it as both itself and the interface it implements. This enables the container to resolve requests for both SauceBéarnaise and IIngredient.

在实际应用中,你总是有不止一个抽象要映射,所以你必须配置多个映射。这是通过多次调用来完成的Register

In real applications, you always have more than one Abstraction to map, so you must configure multiple mappings. This is done with multiple calls to Register:

container.Register<IIngredient, SauceBéarnaise>();
container.Register<ICourse, Course>();

此示例映射IIngredientSauceBéarnaiseICourseCourse没有类型重叠,所以应该很明显发生了什么。但是如果你多次注册相同的抽象会发生什么?

This example maps IIngredient to SauceBéarnaise, and ICourse to Course. There’s no overlap of types, so it should be pretty evident what’s going on. But what would happen if you register the same Abstraction several times?

container.Register<IIngredient, SauceBéarnaise>();
container.Register<IIngredient, Steak>();    ①  

在这里,您注册IIngredient了两次,这导致在第二行抛出异常并显示以下消息:

Here, you register IIngredient twice, which results in an exception being thrown on the second line with the following message:

类型 II 成分已经注册。如果您打算解析 IIngredient 实现的集合,请使用 Collection.Register 重载。有关详细信息,请参阅 https://simpleinjector.org/coll1

Type IIngredient has already been registered. If your intention is to resolve a collection of IIngredient implementations, use the Collection.Register overloads. For more information, see https://simpleinjector.org/coll1.

与大多数其他DI 容器相比,Simple Injector 不允许堆叠注册来构建类型序列,如前面的代码片段所示。它的 API 明确地将序列的注册与单个抽象映射分开。1  而不是多次调用RegisterSimple Injector 强制您使用Collection属性的注册方法,例如Collection.Register

In contrast to most other DI Containers, Simple Injector doesn’t allow stacking up registrations to build up a sequence of types, as the previous code snippet shows. Its API explicitly separates the registration of sequences from single Abstraction mappings.1  Instead of making multiple calls to Register, Simple Injector forces you to use the registration methods of the Collection property, such as Collection.Register:

container.Collection.Register<IIngredient>(
    typeof(SauceBéarnaise),
    typeof(Steak));

此示例在一次调用中注册所有成分。或者,您可以使用Collection.Append将实现添加到一系列成分中:

This example registers all ingredients in one single call. Alternatively, you can use Collection.Append to add implementations to a sequence of ingredients:

container.Collection.Append<IIngredient, SauceBéarnaise>();
container.Collection.Append<IIngredient, Steak>();

通过之前的注册,任何依赖的组件IEnumerable<IIngredient>都会注入一系列成分。Simple Injector 可以很好地处理同一个抽象的多个配置,但我们将在 14.4 节中回到这个主题。

With the previous registrations, any component that depends on IEnumerable<IIngredient> gets a sequence of ingredients injected. Simple Injector handles multiple configurations for the same Abstraction well, but we’ll get back to this topic in section 14.4.

虽然有更高级的选项可用于配置 Simple Injector,但您可以使用此处显示的方法配置整个应用程序。但是,为了避免对容器配置进行过多的显式维护,您可以考虑使用自动注册的更基于约定的方法。

Although there are more-advanced options available for configuring Simple Injector, you can configure an entire application with the methods shown here. But to save yourself from too much explicit maintenance of container configuration, you could instead consider a more convention-based approach using Auto-Registration.

使用自动注册配置容器

Configuring the container using Auto-Registration

在许多情况下,注册将是相似的。这样的注册维护起来很乏味,并且显式注册每个组件可能不是最有效的方法,正如我们在 12.3.3 节中讨论的那样。

In many cases, registrations will be similar. Such registrations are tedious to maintain, and explicitly registering each and every component might not be the most productive approach, as we discussed in section 12.3.3.

考虑一个包含许多IIngredient实现的库。您可以单独配置每个类,但这会导致提供给该方法的Type实例列表不断变化Collection.Register. 更糟糕的是,每次添加新的实现时,如果您希望它可用IIngredient,还必须显式地向 注册它。声明应该注册在给定程序集中找到的Container所有实现会更有成效。IIngredient

Consider a library that contains many IIngredient implementations. You can configure each class individually, but it’ll result in an ever-changing list of Type instances supplied to the Collection.Register method. What’s worse is that, every time you add a new IIngredient implementation, you must also explicitly register it with the Container if you want it to be available. It’d be more productive to state that all implementations of IIngredient found in a given assembly should be registered.

这可以使用一些RegisterCollection.Register方法重载。这些特定的重载允许您指定一个程序集并在单个语句中配置来自该程序集的所有选定类。要获取Assembly实例,可以使用代表类;在这种情况下,Steak

This is possible using some of the Register and Collection.Register method overloads. These particular overloads let you specify an assembly and configure all selected classes from this assembly in a single statement. To get the Assembly instance, you can use a representative class; in this case, Steak:

Assembly ingredientsAssembly = typeof(Steak).Assembly;

container.Collection.Register<IIngredient>(ingredientsAssembly);

前面的示例无条件地配置IIngredient接口的所有实现,但您可以提供使您能够仅选择一个子集的过滤器。这是一个基于约定的扫描,您只添加名称以Sauce开头的类:

The previous example unconditionally configures all implementations of the IIngredient interface, but you can provide filters that enable you to select only a subset. Here’s a convention-based scan where you add only classes whose name starts with Sauce:

Assembly assembly = typeof(Steak).Assembly;

var types = container.GetTypesToRegister<IIngredient>(assembly)
    .Where(type => type.Name.StartsWith("Sauce"));

container.Collection.Register<IIngredient>(types);

此扫描使用该GetTypesToRegister方法, 它搜索类型而不注册它们。这允许您使用谓词过滤选择。现在,您不再使用程序集列表来提供它,而是使用实例Collection.Register列表来提供它。Type

This scan makes use of the GetTypesToRegister method, which searches for types without registering them. This allows you to filter the selection using a predicate. Instead of supplying Collection.Register using a list of assemblies, you now supply it with a list of Type instances.

除了从程序集中选择正确的类型外,自动注册的另一部分是定义正确的映射。在前面的示例中,您使用Collection.Register具有特定接口的方法来针对该接口注册所有选定的类型。然而,有时您可能想要使用不同的约定。假设您使用抽象基类而不是接口,并且您希望在名称以Policy结尾的程序集中按其基类型注册所有类型:

Apart from selecting the correct types from an assembly, another part of Auto-Registration is defining the correct mapping. In the previous examples, you used the Collection.Register method with a specific interface to register all selected types against that interface. Sometimes, however, you may want to use different conventions. Let’s say that instead of interfaces, you use abstract base classes, and you want to register all types in an assembly where the name ends with Policy by their base type:

Assembly policiesAssembly = typeof(DiscountPolicy).Assembly;

var policyTypes =
    from type in policiesAssembly.GetTypes()    ①  
    where type.Name.EndsWith("Policy")    ②  
    select type;

foreach (Type type in policyTypes)
{
    container.Register(type.BaseType, type);    ③  
}

在此示例中,您几乎不使用 Simple Injector API 的任何部分。相反,您可以使用 .NET 框架提供的反射和 LINQ API 来过滤和获取预期的类型。

In this example, you hardly use any part of the Simple Injector API. Instead, you use the reflection and LINQ APIs provided by the .NET framework to filter and get the expected types.

尽管 Simple Injector 基于约定的 API 有限,但通过使用现有的 .NET 框架 API,基于约定的注册仍然非常容易。Simple Injector 基于约定的 API 主要关注序列和泛型类型的注册。当谈到泛型,这就是为什么 Simple Injector 明确支持基于泛型抽象注册类型的原因,我们将在接下来讨论。

Even though Simple Injector’s convention-based API is limited, by making use of existing .NET framework APIs, convention-based registrations are still surprisingly easy. The Simple Injector’s convention-based API mainly focuses around the registration of sequences and generic types. This becomes a different ball game when it comes to generics, which is why Simple Injector has explicit support for registering types based on generic Abstractions, as we’ll discuss next.

通用抽象的自动注册

Auto-Registration of generic Abstractions

在第 10 章的课程中,您重构了令人讨厌的大IProductService界面ICommandService<TCommand>界面清单 10.12。这又是那个抽象

During the course of chapter 10, you refactored the big, obnoxious IProductService interface to the ICommandService<TCommand> interface of listing 10.12. Here’s that Abstraction again:

public interface ICommandService<TCommand>
{
    void Execute(TCommand command);
}

如第 10 章所述,每个命令参数对象代表一个用例,每个用例将有一个实现。以AdjustInventoryService清单 10.8 为例。它实施了“调整库存”用例。下一个清单再次显示了这个类。

As discussed in chapter 10, every command Parameter Object represents a use case, and there’ll be a single implementation per use case. The AdjustInventoryService of listing 10.8 was given as an example. It implemented the “adjust inventory” use case. The next listing shows this class again.

清单 14.2来自第 10 章 的AdjustInventoryService

Listing 14.2 The AdjustInventoryService from chapter 10

public class AdjustInventoryService : ICommandService<AdjustInventory>
{
    private readonly IInventoryRepository repository;

    public AdjustInventoryService(IInventoryRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(AdjustInventory command)
    {
        var productId = command.ProductId;

        ...
    }
}

任何相当复杂的系统都可以轻松实现数百个用例,而这些是使用自动注册的理想选择。使用 Simple Injector,这再简单不过了。

Any reasonably complex system will easily implement hundreds of use cases, and these are ideal candidates for using Auto-Registration. With Simple Injector, this couldn’t be simpler.

清单 14.3 实现的自动注册ICommandService<TCommand>

Listing 14.3 Auto-Registration of ICommandService<TCommand> implementations

Assembly assembly = typeof(AdjustInventoryService).Assembly;

container.Register(typeof(ICommandService<>), assembly);

与之前使用的清单不同Collection.Register,您再次使用Register. 这是因为请求的命令服务总是只有一个实现;您不想注入一系列命令服务。

In contrast to the previous listing that used Collection.Register, you again make use of Register. This is because there’ll always be exactly one implementation of a requested command service; you don’t want to inject a sequence of command services.

使用提供的开放通用接口,Simple Injector 遍历程序集类型列表并注册实现封闭通用版本的ICommandService<TCommand>. 例如,这意味着它AdjustInventoryService已注册,因为它实现ICommandService<AdjustInventory>了 ,它是 . 的封闭通用版本ICommandService<TCommand>

Using the supplied open-generic interface, Simple Injector iterates through the list of assembly types and registers types that implement a closed-generic version of ICommandService<TCommand>. What this means, for instance, is that AdjustInventoryService is registered because it implements ICommandService<AdjustInventory>, which is a closed-generic version of ICommandService<TCommand>.

不过,并不是所有ICommandService<TCommand>的实现都会被注册。Simple Injector 跳过了开放式通用实现、装饰器和合成器,因为它们通常需要特殊注册。我们将在 14.4 节中讨论这个问题。

Not all ICommandService<TCommand> implementations will be registered, though. Simple Injector skips open-generic implementations, Decorators, and Composites, as they often require special registration. We’ll discuss this in section 14.4.

Register方法采用一paramsAssembly实例,因此您可以根据需要为单个约定提供任意数量的程序集。扫描文件夹中的程序集并提供所有程序集以实现加载项功能并不是一个牵强附会的想法,无需重新编译核心应用程序即可添加加载项。(有关示例,请参阅https://simpleinjector.org/registering-plugins-dynamically。)这是实现后期绑定的一种方法;另一种是使用配置文件。

The Register method takes a params array of Assembly instances, so you can supply as many assemblies as you like to a single convention. It’s not a far-fetched idea to scan a folder for assemblies and supply them all to implement add-in functionality where add-ins can be added without recompiling a core application. (For an example, see https://simpleinjector.org/registering-plugins-dynamically.) This is one way to implement late binding; another is to use configuration files.

使用配置文件配置容器

Configuring the container using configuration files

当您需要能够在不重新编译应用程序的情况下更改配置时,配置文件是一个不错的选择。使用配置文件最自然的方法是将它们嵌入到标准的 .NET 应用程序配置文件中。这是可能的,但如果您需要能够独立于标准 .config 文件改变 Simple Injector 配置,您也可以使用独立的配置文件。

When you need to be able to change a configuration without recompiling the application, configuration files are a good option. The most natural way to use configuration files is to embed them into a standard .NET application configuration file. This is possible, but you can also use a standalone configuration file if you need to be able to vary the Simple Injector configuration independently of the standard .config file.

如本节开头所述,Simple Injector 没有明确支持基于文件的配置。但是,通过使用 .NET Core 的内置配置系统,从配置文件加载注册非常简单。为此,您可以定义自己的配置结构,将抽象映射到实现。IIngredient下面是一个将接口映射到类的简单示例Steak

As stated in the beginning of this section, there’s no explicit support in Simple Injector for file-based configuration. By making use of .NET Core’s built-in configuration system, however, loading registrations from a configuration file is rather straightforward. For this purpose, you can define your own configuration structure that maps Abstractions to implementations. Here’s a simple example that maps the IIngredient interface to the Steak class.

清单 14.4IIngredient从到Steak使用配置文件的 简单映射

Listing 14.4 Simple mapping from IIngredient to Steak using a configuration file

{
  "registrations": [
    {
      "service":
        "Ploeh.Samples.MenuModel.IIngredient, Ploeh.Samples.MenuModel",
      "implementation":
        "Ploeh.Samples.MenuModel.Steak, Ploeh.Samples.MenuModel"
    }
  ]
}

registrations元素是一个 JSONregistration元素数组。前面的示例包含单个注册,但您可以根据需要添加任意多个registration元素。在每个元素中,您必须指定具有implementation属性的具体类型. 要将Steak类映射到IIngredient,您可以使用service属性。

The registrations element is a JSON array of registration elements. The previous example contained a single registration, but you can add as many registration elements as you like. In each element, you must specify a concrete type with the implementation attribute. To map the Steak class to IIngredient, you can use the service attribute.

使用 .NET Core 的内置配置系统,您可以加载配置文件并循环访问它。然后将定义的注册附加到容器:

Using .NET Core’s built-in configuration system, you can load a configuration file and iterate through it. You then append the defined registrations to the container:

var config = new ConfigurationBuilder()    ①  
    .AddJsonFile("simpleinjector.json")    ①  
    .Build();    ①  

var registrations = config    ②  
    .GetSection("registrations").GetChildren();    ②  

foreach (var reg in registrations)    ③  
{    ③  
    container.Register(    ③  
        Type.GetType(reg["service"]),    ③  
        Type.GetType(reg["implementation"]));    ③  
}

当您需要在不重新编译应用程序的情况下更改一个或多个组件的配置时,配置文件是一个不错的选择,但由于它往往非常脆弱,您应该只为那些场合保留它并使用自动注册配置作为容器配置主要部分的代码

A configuration file is a good option when you need to change the configuration of one or more components without recompiling the application, but because it tends to be quite brittle, you should reserve it for only those occasions and use either Auto-Registration or Configuration as Code for the main part of the container’s configuration.

本节介绍了 Simple Injector DI 容器并演示了这些基本机制:如何配置Container,以及随后如何使用它来解析服务。只需调用一次GetInstance方法即可轻松完成解析服务,因此复杂性涉及配置容器。这可以通过几种不同的方式完成,包括命令式代码和配置文件。

This section introduced the Simple Injector DI Container and demonstrated these fundamental mechanics: how to configure a Container, and, subsequently, how to use it to resolve services. Resolving services is easily done with a single call to the GetInstance method, so the complexity involves configuring the container. This can be done in several different ways, including imperative code and configuration files.

到目前为止,我们只了解了最基本的 API;我们还没有涵盖更先进的领域。最重要的主题之一是如何管理组件的生命周期。

Until now, we’ve only looked at the most basic API; we have yet to cover more-advanced areas. One of the most important topics is how to manage component lifetime.

14.2 管理生命周期

14.2 Managing lifetime

在第 8 章中,我们讨论了生命周期管理,包括最常见的概念生命周期,例如TransientSingletonScoped。Simple Injector 的Lifestyle支持映射到这三种Lifestyles表 14.2中显示的生活方式作为 API 的一部分提供。

In chapter 8, we discussed Lifetime Management, including the most common conceptual Lifestyles such as Transient, Singleton, and Scoped. Simple Injector’s Lifestyle supports mapping to these three Lifestyles. The Lifestyles shown in table 14.2 are available as part of the API.

表 14.2 简单的注射器生活方式
简单注入器名称花样名称注释
短暂的短暂的这是默认的生活方式瞬态实例不会被容器跟踪,因此永远不会被丢弃。使用其诊断服务,Simple Injector 会在您将一次性组件注册为Transient时发出警告。
单例单例实例在容器被处置时被处置。
范围范围允许范围实例的生活方式模板。Scoped Lifestyle由基类定义,ScopedLifestyle有多种ScopedLifestyle实现。.NET Core 应用程序最常用的生活方式AsyncScopedLifestyle. 在范围的生命周期内跟踪实例,并在范围被处置时被处置。

Simple Injector 对TransientSingleton的实现相当于第 8 章中描述的一般Lifestyles,因此本章我们不会花太多时间在它们上面。相反,在本节中,您将看到如何在代码中为组件定义Lifestyles 。我们还将了解 Simple Injector 的环境作用域概念,以及它如何简化容器的使用。然后,我们将介绍 Simple Injector 如何验证和诊断其配置以防止常见的配置错误。到本节结束时,您应该能够在自己的应用程序中使用 Simple Injector 的Lifestyles 。让我们首先回顾一下如何为组件配置Lifestyles

Simple Injector’s implementations of Transient and Singleton are equivalent to the general Lifestyles described in chapter 8, so we won’t spend much time on them in this chapter. Instead, in this section, you’ll see how you can define Lifestyles for components in code. We’ll also look at Simple Injector’s concept of ambient scoping and how it can simplify working with the container. We’ll then cover how Simple Injector can verify and diagnose its configuration to prevent common configuration errors. By the end of this section, you should be able to use Simple Injector’s Lifestyles in your own application. Let’s start by reviewing how to configure Lifestyles for components.

14.2.1 配置生活方式

14.2.1 Configuring Lifestyles

在本节中,我们将回顾如何使用 Simple Injector管理生活方式。生活方式被配置为注册组件的一部分。就这么简单:

In this section, we’ll review how to manage Lifestyles with Simple Injector. A Lifestyle is configured as part of registering components. It’s as easy as this:

container.Register<SauceBéarnaise>(Lifestyle.Singleton);

此示例将具体SauceBéarnaise类配置为单例,以便每次SauceBéarnaise请求时返回相同的实例。如果你想将抽象映射到具有特定生命周期的具体类,你可以使用Register带有两个通用参数的通常重载,同时为其提供Lifestyle.Singleton

This example configures the concrete SauceBéarnaise class as a Singleton so that the same instance is returned each time SauceBéarnaise is requested. If you want to map an Abstraction to a concrete class with a specific lifetime, you can use the usual Register overload with two generic arguments, while supplying it with the Lifestyle.Singleton:

container.Register<IIngredient, SauceBéarnaise>(Lifestyle.Singleton);

虽然Transient是默认的Lifestyle,但您可以明确声明它。这两个例子在默认配置下是等价的:2 

Although Transient is the default Lifestyle, you can explicitly state it. These two examples are equivalent under the default configuration:2 

container.Register<IIngredient, SauceBéarnaise>(    ①  
    Lifestyle.Transient);    ①  

container.Register<IIngredient, SauceBéarnaise>();  ②  

可以通过多种方式为基于约定的注册配置Lifestyles 。例如,当注册一个序列时,其中一个选项是提供Collection.Register方法带有Registration实例列表:

Configuring Lifestyles for convention-based registrations can be done in several ways. When registering a sequence, for instance, one of the options is to supply the Collection.Register method with a list of Registration instances:

Assembly assembly = typeof(Steak).Assembly;

var types = container.GetTypesToRegister<IIngredient>(assembly);

container.Collection.Register<IIngredient>(
    from type in types
    select Lifestyle.Singleton.CreateRegistration(type, container));

您可以使用Lifestyle.Singleton定义公约中所有注册的生活方式。在此示例中,您将所有IIngredient注册定义为单例,方法是将它们全部作为Registration实例提供给Collection.Register重载。3个 

You can use Lifestyle.Singleton to define the Lifestyle for all registrations in a convention. In this example, you define all IIngredient registrations as Singleton by supplying them all as a Registration instance to the Collection.Register overload.3 

在为组件配置Lifestyles时,有很多选择。在所有情况下,它都是以一种相当声明的方式完成的。尽管配置通常很容易,但您一定不要忘记一些Lifestyles涉及长期存在的对象,只要它们存在就会使用资源。

When it comes to configuring Lifestyles for components, there are many options. In all cases, it’s done in a rather declarative fashion. Although configuration is typically easy, you mustn’t forget that some Lifestyles involve long-lived objects, which use resources as long as they’re around.

14.2.2 释放组件

14.2.2 Releasing components

正如 8.2.2 节中所讨论的,当你用完它们时释放对象是很重要的。与 Autofac 类似,Simple Injector 没有显式Release方法,而是使用一个名为scopes的概念。范围可以被视为特定于请求的缓存。如图14.3所示,它定义了组件可以重用的边界。

As discussed in section 8.2.2, it’s important to release objects when you’re done with them. Similar to Autofac, Simple Injector has no explicit Release method, but instead uses a concept called scopes. A scope can be regarded as a request-specific cache. As figure 14.3 illustrates, it defines a boundary where components can be reused.

14-03.eps

图 14.3 简单注入器Scope充当特定于请求的缓存,可以在有限的持续时间或目的内共享组件。

Figure 14.3 Simple Injector’s Scope acts as a request-specific cache that can share components for a limited duration or purpose.

AScope定义了一个缓存,您可以将其用于特定的持续时间或目的;最明显的例子是网络请求。当从 a 请求范围内的组件时Scope,您总是收到相同的实例。与真正的单例的不同之处在于,如果您查询第二个范围,您将获得另一个实例。

A Scope defines a cache that you can use for a particular duration or purpose; the most obvious example is a web request. When a scoped component is requested from a Scope, you always receive the same instance. The difference from true Singletons is that if you query a second scope, you’ll get another instance.

作用域的一个重要特性是它们允许您在作用域完成时正确地释放组件。BeginScope您使用特定实现的方法创建一个新范围,ScopedLifestyle并通过调用其Dispose方法释放所有适当的组件:

One of the important features of scopes is that they let you properly release components when the scope completes. You create a new scope with the BeginScope method of a particular ScopedLifestyle implementation and release all appropriate components by invoking its Dispose method:

using (AsyncScopedLifestyle.BeginScope(container))    ①  
{
    IMeal meal = container.GetInstance<IMeal>();    ②  

    meal.Consume();    ③  

}    ④  

此示例显示如何IMealContainer实例中解析,而不是从Scope实例中解析。这不是打字错误——容器自动“知道”它在哪个活动范围内运行。下一节将对此进行更详细的讨论。

This example shows how IMeal is resolved from the Container instance, instead of being resolved from a Scope instance. This isn’t a typo — the container automatically “knows” in which active scope it’s operating. The next section discusses this in more detail.

BeginScope在前面的示例中,通过调用方法创建了一个新范围在相应的Scoped Lifestyle上。返回值 implements IDisposable,因此您可以将其包装在一个using块中。

In the previous example, a new scope is created by invoking the BeginScope method on the corresponding Scoped Lifestyle. The return value implements IDisposable, so you can wrap it in a using block.

当你用完一个范围后,你可以用一个using块来处理它。当您退出该块时,这会自动发生,但您也可以选择通过调用Dispose方法来显式处理它. 当您处置范围时,您还释放了在该范围内创建的所有组件。在这个例子中,这意味着你释放了餐点对象图。

When you’re done with a scope, you can dispose of it with a using block. This happens automatically when you exit the block, but you can also choose to explicitly dispose of it by invoking the Dispose method. When you dispose of a scope, you also release all the components that were created during that scope. In the example, it means that you release the meal object graph.

在本节的前面,您看到了如何将组件配置为SingletonsTransients。配置组件以将其Lifestyle绑定到范围以类似的方式完成:

Earlier in this section, you saw how to configure components as Singletons or Transients. Configuring a component to have its Lifestyle tied to a scope is done in a similar way:

container.Register<IIngredient, SauceBéarnaise>(Lifestyle.Scoped);

Lifestyle.Singleton与and类似,您可以使用该值来声明组件的生命周期应该在创建实例的范围内存在。但是,这个调用本身会导致容器抛出以下异常:Lifestyle.TransientLifestyle.Scoped

Similar to Lifestyle.Singleton and Lifestyle.Transient, you can use the Lifestyle.Scoped value to state that the component’s lifetime should live for the duration of the scope that created the instance. This call by itself, however, would cause the container to throw the following exception:

为了能够使用 Lifestyle.Scoped 属性,请确保容器配置了默认的范围生活方式,方法是将 Container.Options.DefaultScopedLifestyle 属性设置为您的应用程序类型所需的范围生活方式。有关详细信息,请参阅https://simpleinjector.org/scoped

To be able to use the Lifestyle.Scoped property, please ensure that the container is configured with a default scoped lifestyle by setting the Container.Options.DefaultScopedLifestyle property with the required scoped lifestyle for your type of application. For more information, see https://simpleinjector.org/scoped.

在您可以使用该Lifestyle.Scoped值之前,Simple Injector 要求您设置该Container.Options.DefaultScopedLifestyle属性。Simple Injector 有多个ScopedLifestyle实现,有时特定于一个框架。这意味着您必须明确配置ScopedLifestyle最适合您的应用程序类型的实现。对于 ASP.NET Core 应用程序,正确的Scoped LifestyleAsyncScopedLifestyle,您可以像这样配置它:

Before you can use the Lifestyle.Scoped value, Simple Injector requires that you set the Container.Options.DefaultScopedLifestyle property. Simple Injector has multiple ScopedLifestyle implementations that are sometimes specific to a framework. This means you’ll have to explicitly configure the ScopedLifestyle implementation that works best for your type of application. For ASP.NET Core applications, the proper Scoped Lifestyle is the AsyncScopedLifestyle, which you can configure like this:

var container = new Container();

container.Options.DefaultScopedLifestyle =    ①  
    new AsyncScopedLifestyle();    ①  

container.Register<IIngredient, SauceBéarnaise>(
    Lifestyle.Scoped);    ②  

由于它们的性质,单例在容器本身的生命周期内永远不会被释放。尽管如此,如果您不再需要容器,您甚至可以释放这些组件。这是通过处理容器本身来完成的:

Due to their nature, Singletons are never released for the lifetime of the container itself. Still, you can release even those components if you don’t need the container any longer. This is done by disposing of the container itself:

container.Dispose();

实际上,这并不像处理范围那么重要,因为容器的生命周期往往与其支持的应用程序的生命周期密切相关。只要应用程序运行,您通常会保留容器,因此只有在应用程序关闭时才处理它。在这种情况下,内存将被操作系统回收。

In practice, this isn’t nearly as important as disposing of a scope, because the lifetime of a container tends to correlate closely with the lifetime of the application it supports. You normally keep the container around as long as the application runs, so you only dispose of it when the application shuts down. In this case, memory would be reclaimed by the operating system.

正如我们在本节前面提到的,使用简单注入器,您总是从容器中解析对象——而不是从范围中。这是有效的,因为范围在简单注入器中是环境的。接下来让我们看看环境作用域。

As we mentioned earlier in this section, with Simple Injector, you always resolve objects from the container — not from a scope. This works because scopes are ambient in Simple injector. Let’s look at ambient scopes next.

14.2.3 环境作用域

14.2.3 Ambient scopes

使用 Simple Injector,之前创建和处理作用域的示例展示了如何始终从 解析实例Container,即使您解析的是作用域实例。以下示例再次显示了这一点:

With Simple Injector, the previous example of the creation and disposal of the scope shows how you can always resolve instances from the Container, even if you resolve scoped instances. The following example shows this again:

using (AsyncScopedLifestyle.BeginScope(container))
{
    IMeal meal = container.GetInstance<IMeal>();    ①  

    meal.Consume();
}

这揭示了 Simple Injector 的一个有趣特性,即作用域实例是环境实例,并且在它们运行的​​上下文中全局可用。以下清单显示了此行为。

This reveals an interesting feature of Simple Injector, which is that scope instances are ambient and are globally available in the context in which they’re running. The following listing shows this behavior.

清单 14.5 环境范围在简单的注射器中

Listing 14.5 Ambient scopes in Simple Injector

var container = new Container();

container.Options.DefaultScopedLifestyle =
    new AsyncScopedLifestyle();

Scope scope1 = Lifestyle.Scoped    ①  
    .GetCurrentScope(container);    ①  

using (Scope scope2 =
    AsyncScopedLifestyle.BeginScope(container))
{
    Scope scope3 = Lifestyle.Scoped    ②  
        .GetCurrentScope(container);    ②  
}

Scope scope4 = Lifestyle.Scoped    ③  
    .GetCurrentScope(container);    ③  

此行为类似于 .NETTransactionScope类的行为. 4  当您用 包装操作时TransactionScope,在该操作中打开的所有数据库连接将自动成为同一事务的一部分。

This behavior is similar to that of .NET’s TransactionScope class.4  When you wrap an operation with a TransactionScope, all database connections opened within that operation will automatically be part of the same transaction.

通常,您不会使用该GetCurrentScope方法很多,如果有的话。当Container您开始解析实例时,代表您在后台使用它。不过,它很好地展示了Scope可以从容器中检索和访问实例。

In general, you won’t use the GetCurrentScope method a lot, if at all. The Container uses this under the hood on your behalf when you start resolving instances. Still, it demonstrates nicely that Scope instances can be retrieved and are accessible from the container.

一个ScopedLifestyle实现,例如之前的,存储它创建的实例供以后使用,这允许在相同的上下文中检索它。它是定义代码何时在同一上下文中运行的特定实现。例如,将内部存储在一个. 5  这允许范围从一个方法流向另一个方法,即使异步方法在不同的线程上继续,如本例所示:AsyncScopedLifestyleScopeScopedLifestyleAsyncScopedLifestyleScopeSystem.Threading.AsyncLocal<T>

A ScopedLifestyle implementation, such as the previous AsyncScopedLifestyle, stores its created Scope instance for later use, which allows it to be retrieved within the same context. It’s the particular ScopedLifestyle implementation that defines when code runs in the same context. The AsyncScopedLifestyle, for instance, stores the Scope internally in an System.Threading.AsyncLocal<T>.5  This allows scopes to flow from method to method, even if an asynchronous method continues on a different thread, as this example demonstrates:

using (AsyncScopedLifestyle.BeginScope(container))
{
    IMeal meal = container.GetInstance<IMeal>();

    await meal.Consume();    ①  

    meal = container.GetInstance<IMeal>();    ②  
}

尽管环境范围一开始可能会令人困惑,但它们的使用通常会简化 Simple Injector 的使用。例如,您不必担心从容器解析时出现内存泄漏,因为 Simple Injector 会代表您透明地管理它。Scope实例永远不会缓存在根容器中,这是您在使用本书中描述的其他容器时需要谨慎的一点。Simple Injector 擅长的另一个领域是检测常见错误配置的能力。

Although ambient scopes might be confusing at first, their usage typically simplifies working with Simple Injector. For instance, you won’t have to worry about getting memory leaks when resolving from the container, because Simple Injector manages this transparently on your behalf. Scope instances will never be cached in the root container, which is something you need to be cautious about with the other containers described in this book. Another area in which Simple Injector excels is the ability to detect common misconfigurations.

14.2.4 诊断容器的常见生命周期问题

14.2.4 Diagnosing the container for common lifetime problems

Pure DI相比,在DI Container中注册和构建对象图更加隐式。这使得很容易意外地错误配置容器。为此,很多DI Container都有一个功能,可以让所有的注册都可以迭代,从而验证是否都可以解析,Simple Injector也不例外。

Compared to Pure DI, registration and building object graphs in a DI Container is more implicit. This makes it easy to accidentally misconfigure the container. For that reason, many DI Containers have a function that allows all registrations to be iterated to enable verifying whether all can be resolved, and Simple Injector is no exception.

然而,能够解析一个对象图并不能保证配置的正确性,正如第 8.4.1 节的Captive Dependency陷阱所说明的那样。Captive Dependency是组件生命周期的错误配置。事实上,大多数有关使用DI 容器的错误都与生命周期错误配置有关。

Being able to resolve an object graph, however, is no guarantee of the correctness of the configuration, as the Captive Dependency pitfall of section 8.4.1 illustrates. A Captive Dependency is a misconfiguration of the lifetime of a component. In fact, most errors concerning working with DI Containers are related to lifetime misconfigurations.

由于DI 容器错误配置非常普遍且通常难以追踪,Simple Injector 允许您验证其配置,这超出了大多数DI 容器支持的对象图的简单实例化。最重要的是,Simple Injector 会扫描对象图以查找常见的错误配置——Captive Dependencies就是其中之一。

Because DI Container misconfigurations are so common and often difficult to trace, Simple Injector lets you verify its configuration, which goes beyond the simple instantiation of object graphs that most DI Containers support. On top of that, Simple Injector scans the object graphs for common misconfigurations — Captive Dependencies being one of them.

因此,Simple Injector 的两步过程的配置步骤,如图 14.1 所示,存在两个子步骤。图 14.4显示了这个过程。

Therefore, the configure step of Simple Injector’s two-step process, as outlined in figure 14.1, exists of two substeps. Figure 14.4 shows this process.

14-04.eps

图 14.4 使用 Simple Injector 的模式是配置它,包括验证它,然后解析组件。

Figure 14.4 The pattern for using Simple Injector is to configure it, including verifying it, and then to resolve components.

让 Simple Injector 诊断和检测配置错误的最简单方法是调用ContainerVerify方法,如以下清单所示。

The easiest way to let Simple Injector diagnose and detect configuration errors is by calling the Container’s Verify method, as shown in the following listing.

清单 14.6 验证容器

Listing 14.6 Verifying the container

var container = new Container();

container.Register<IIngredient, Steak>();  ①  

container.Verify();    ②  

让容器检测Captive Dependencies

Letting the container detect Captive Dependencies

Captive Dependency错误配置是 Simple Injector 检测到的错误配置。现在让我们看看如何使用第 14.1.1 节中的成分导致俘虏依赖性Verify跳闸。它的构造函数包含两个依赖项Mayonnaise

The Captive Dependency misconfiguration is one that Simple Injector detects. Now let’s see how you can cause Verify to trip on a Captive Dependency using the Mayonnaise ingredient of section 14.1.1. Its constructor contained two Dependencies:

public Mayonnaise(EggYolk eggYolk, SunflowerOil oil)

下面的清单注册Mayonnaise了它的两个Dependencies。但是它错误配置MayonnaiseSingleton,而它的EggYolk Dependency被注册为Transient

The following listing registers Mayonnaise with its two Dependencies. But it misconfigures Mayonnaise as Singleton, whereas its EggYolk Dependency is registered as Transient.

清单 14.7 使容器检测俘虏依赖

Listing 14.7 Causing the container to detect a Captive Dependency

var container = new Container();

container.Register<EggYolk>(Lifestyle.Transient);    ①  
container.Register<Mayonnaise>(Lifestyle.Singleton);  ②  
container.Register<SunflowerOil>(Lifestyle.Singleton);

container.Verify();    ③  

当您调用时Register,Simple Injector 仅执行一些基本的验证。这包括检查该类型是否是抽象的,它是否具有公共构造函数等。它不会在该阶段检查诸如Captive Dependencies之类的问题,因为可以按任意顺序进行注册。例如,在清单 14.7SunflowerOil中,它是在 之后注册Mayonnaise的,即使它是 的依赖Mayonnaise。这样做是完全正确的。只有配置完成后,才能进行验证。当您运行此代码示例时,对 的调用Verify失败并显示以下异常消息:

When you call Register, Simple Injector only performs some rudimentary validations. This includes checking that the type isn’t abstract, that it has a public constructor, and the like. It won’t check for problems such as Captive Dependencies at that stage, because registrations can be made in any arbitrary order. In listing 14.7, for instance, SunflowerOil is registered after Mayonnaise, even though it’s a Dependency of Mayonnaise. It’s completely valid to do so. It’s only after the configuration is completed that verification can be performed. When you run this code example, the call to Verify fails with the following exception message:

配置无效。报告了以下诊断警告:

The configuration is invalid. The following diagnostic warnings were reported:

-[生活方式不匹配] 蛋黄酱(单身)取决于蛋黄(瞬态)。有关警告的详细信息,请参阅错误属性。请参阅https://simpleinjector.org/diagnostics如何解决问题以及如何抑制个别警告。

-[Lifestyle Mismatch] Mayonnaise (Singleton) depends on EggYolk (Transient). See the Error property for detailed information about the warnings. Please see https://simpleinjector.org/diagnostics how to fix problems and how to suppress individual warnings.

这里一个有趣的观察是,Simple Injector 不允许将Transient Dependencies注入到Singleton消费者中。这与 Autofac 相反。使用 Autofac,Transients被隐含地期望与它们的消费者一样长,这意味着在 Autofac 中,这种情况永远不会被视为Captive Dependency。出于这个原因,Autofac 调用了一个Transient ,它几乎描述了它的行为:配置为Transient的每个消费者的Dependency都应该得到它自己的实例。正因为如此,Autofac 只检测将作用域组件注入到单例InstancePerDependency俘虏依赖

An interesting observation here is that Simple Injector doesn’t allow Transient Dependencies to be injected into Singleton consumers. This is the opposite of Autofac. With Autofac, Transients are implicitly expected to live as long as their consumer, which means that in Autofac, this situation is never considered to be a Captive Dependency. For that reason, Autofac calls a Transient InstancePerDependency, which pretty much describes its behavior: each consumer’s Dependency that’s configured as Transient is expected to get its own instance. Because of that, Autofac only detects the injection of scoped components into Singletons as Captive Dependencies.

尽管这有时可能正是您需要的行为,但在大多数情况下并非如此。更常见的是,Transient组件预计会存在很短的一段时间,而将它们注入Singleton consumer 会导致组件在应用程序存在时一直存在。因此,Simple Injector 的座右铭是:“安全胜于遗憾”,这就是它抛出异常的原因。有时您可能需要在您最了解的情况下抑制此类警告。

Although this might sometimes be exactly the behavior you need, in most cases, it’s not. More often, Transient components are expected to live for a brief period of time, whereas injecting them into a Singleton consumer causes the component to live for as long as the application lives. Because of this, Simple Injector’s motto is: “better safe than sorry,” which is why it throws an exception. Sometimes you might need to suppress such warnings in cases where you know best.

抑制对个人注册的警告

Suppressing warnings on individual registrations

如果您想忽略EggYolk的到期日期,Simple Injector 可以让您取消对该特定注册的检查。

In case you want to ignore EggYolk’s expiration date, Simple Injector lets you suppress the check on that particular registration.

清单 14.8 抑制诊断警告

Listing 14.8 Suppressing a diagnostic warning

var container = new Container();

Registration reg = Lifestyle.Transient    ①  
    .CreateRegistration<EggYolk>(container);    ①  

reg.SuppressDiagnosticWarning(    ②  
    DiagnosticType.LifestyleMismatch,    ②  
    justification: "I like to eat rotten eggs.");  ②  

container.AddRegistration(typeof(EggYolk), reg);    ③  

container.Register<Mayonnaise>(Lifestyle.Singleton);
container.Register<SunflowerOil>(Lifestyle.Singleton);

container.Verify();

SuppressDiagnosticWarning包含必需的justification参数. 它根本不被使用,但用作提醒,这样您就不会忘记记录警告被抑制的原因。SuppressDiagnosticWarning

SuppressDiagnosticWarning contains a required justification argument. It isn’t used by SuppressDiagnosticWarning at all, but serves as a reminder so that you don’t forget to document why the warning is suppressed.

我们的使用简单喷油器的生命周期管理之旅到此结束。组件可以使用混合的Lifestyles配置,当您注册同一个Abstraction的多个实现时也是如此。

This completes our tour of Lifetime Management with Simple Injector. Components can be configured with mixed Lifestyles, and this is even true when you register multiple implementations of the same Abstraction.

到目前为止,您已经通过隐式假设所有组件都使用构造函数注入来允许容器连接依赖项。但情况并非总是如此。在下一节中,我们将回顾如何处理必须以特殊方式实例化的类。

Until now, you’ve allowed the container to wire Dependencies by implicitly assuming that all components use Constructor Injection. But this isn’t always the case. In the next section, we’ll review how to deal with classes that must be instantiated in special ways.

14.3 注册困难的 API

14.3 Registering difficult APIs

到目前为止,我们已经考虑了如何配置使用构造函数注入的组件。构造函数注入的众多好处之一是像 Simple Injector 这样的DI 容器可以轻松理解如何在依赖图中组合和创建所有类。当 API 表现不佳时,这一点就不太清楚了。

Until now, we’ve considered how you can configure components that use Constructor Injection. One of the many benefits of Constructor Injection is that DI Containers like Simple Injector can easily understand how to compose and create all classes in a Dependency graph. This becomes less clear when APIs are less well behaved.

在本节中,您将看到如何处理原始构造函数参数和静态工厂。这些都需要特别注意。让我们首先看一下采用基本类型(例如字符串或整数)作为构造函数参数的类。

In this section, you’ll see how to deal with primitive constructor arguments and static factories. These all require special attention. Let’s start by looking at classes that take primitive types, such as strings or integers, as constructor arguments.

14.3.1 配置原始依赖

14.3.1 Configuring primitive Dependencies

只要您将抽象注入消费者,一切都很好。但是,当构造函数依赖于基本类型(例如字符串、数字或枚举)时,这就变得更加困难。对于将连接字符串作为构造函数参数的数据访问实现尤其如此,但这是一个更普遍的问题,适用于所有字符串和数字类型。

As long as you inject Abstractions into consumers, all is well. But it becomes more difficult when a constructor depends on a primitive type, such as a string, a number, or an enum. This is particularly the case for data access implementations that take a connection string as constructor parameter, but it’s a more general issue that applies to all string and numeric types.

从概念上讲,将字符串或数字注册为容器中的组件没有任何意义。特别是,当使用自动装配时,原始类型的注册会导致歧义。以字符串为例。一个组件可能需要数据库连接字符串,而另一个组件可能需要文件路径。这两者在概念上是不同的,但是因为自动装配是通过根据类型选择依赖关系来工作的,所以它们变得模棱两可。出于这个原因,Simple Injector 阻止了原始Dependencies的注册。以这个构造函数为例:

Conceptually, it doesn’t make sense to register a string or number as a component in a container. In particular, when Auto-Wiring is used, the registration of primitive types causes ambiguity. Take string, for instance. Where one component might require a database connection string, another might require a file path. The two are conceptually different, but because Auto-Wiring works by selecting Dependencies based on their type, they become ambiguous. For that reason, Simple Injector blocks the registration of primitive Dependencies. Consider as an example this constructor:

public ChiliConCarne(Spiciness spiciness)

在这个例子中,Spiciness是一个枚举:

In this example, Spiciness is an enum:

public enum Spiciness { Mild, Medium, Hot }

您可能想像ChiliConCarne下面的示例那样进行注册。那行不通的!

You might be tempted to register ChiliConCarne as in the following example. That won’t work!

container.Register<ICourse, ChiliConCarne>();

此行导致异常并显示以下消息:

This line causes an exception with the following message:

ChiliConCarne 类型的构造函数包含 Spiciness 类型的参数 'spiciness',由于它是值类型,因此不能用于构造函数注入。

The constructor of type ChiliConCarne contains parameter 'spiciness' of type Spiciness, which cannot be used for constructor injection because it’s a value type.

当你想ChiliConCarne用 medium解决时Spiciness,你必须离开Auto-Wiring并使用委托:7 

When you want to resolve ChiliConCarne with a medium Spiciness, you’ll have to depart from Auto-Wiring and instead use a delegate:7 

container.Register<ICourse>(() => new ChiliConCarne(Spiciness.Medium));

使用委托的缺点是当ChiliConCarne构造函数更改时必须更改注册。例如,当您向构造函数添加IIngredient 依赖ChiliConCarne项时,必须更新注册:

The downside of using delegates is that the registration has to be changed when the ChiliConCarne constructor changes. When you add an IIngredient Dependency to the ChiliConCarne constructor, for instance, the registration must be updated:

container.Register<ICourse>(() =>    ①  
    new ChiliConCarne(
        Spiciness.Medium,
        container.GetInstance<IIngredient>()));    ②  

除了Composition Root中的额外维护之外,由于缺乏自动装配,委托的使用不允许简单注入器验证关系ChiliConCarne及其IIngredient 依赖关系的有效性。委托隐藏了此依赖项存在的事实。这并不总是一个问题,但它会使由于错误配置引起的诊断问题变得复杂。由于这些缺点,更方便的解决方案是将原始依赖项提取到参数对象中。

Besides the additional maintenance in the Composition Root, and because of the lack of Auto-Wiring, the use of delegates disallows Simple Injector from verifying the validity of the relationship between ChiliConCarne and its IIngredient Dependency. The delegate hides the fact that this Dependency exists. This isn’t always a problem, but it can complicate diagnosing problems that are caused due to misconfigurations. Because of these downsides, a more convenient solution is to extract the primitive Dependencies into Parameter Objects.

14.3.2 提取对参数对象的原始依赖

14.3.2 Extracting primitive Dependencies to Parameter Objects

在第 10.3.3 节中,我们讨论了参数对象的引入如何减轻导致的开闭原则违规IProductService。然而,参数对象也是减少歧义的好工具。例如,Spiciness可以用更笼统的术语将课程描述为调味品。调味可能包括其他属性,例如咸味,因此您可以将Spiciness咸味包装在一个Flavoring类中:

In section 10.3.3, we discussed how the introduction of Parameter Objects allowed mitigating the Open/Closed Principle violation that IProductService caused. Parameter Objects, however, are also a great tool to mitigate ambiguity. For example, the Spiciness of a course could be described in more general terms as a flavoring. Flavoring might include other properties, such as saltiness, so you can wrap Spiciness and the saltiness in a Flavoring class:

public class Flavoring
{
    public readonly Spiciness Spiciness;
    public readonly bool ExtraSalty;

    public Flavoring(Spiciness spiciness, bool extraSalty)
    {
        this.Spiciness = spiciness;
        this.ExtraSalty = extraSalty;
    }
}

正如我们在 10.3.3 节中提到的,Parameter Objects 有一个参数是完全没问题的。目标是消除歧义,而不仅仅是在技术层面上。这样的参数对象的名称可能会更好地描述您的代码在功能级别上所做的事情,就像Flavoring该类所做的那样优雅。随着Flavoring参数对象的引入,现在可以自动连接任何ICourse需要一些修饰的实现:

As we mentioned in section 10.3.3, it’s perfectly fine for Parameter Objects to have one parameter. The goal is to remove ambiguity, and not just on the technical level. Such a Parameter Object’s name might do a better job describing what your code does on a functional level, as the Flavoring class so elegantly does. With the introduction of the Flavoring Parameter Object, it now becomes possible to Auto-Wire any ICourse implementation that requires some flavoring:

var flavoring = new Flavoring(Spiciness.Medium, extraSalty: true);
container.RegisterInstance<Flavoring>(flavoring);

container.Register<ICourse, ChiliConCarne>();

此代码创建该类的单个实例FlavoringFlavoring成为课程的配置对象。因为只有一个Flavoring实例,所以您可以使用RegisterInstance.

This code creates a single instance of the Flavoring class. Flavoring becomes a configuration object for courses. Because there’ll only be one Flavoring instance, you can register it in Simple Injector using RegisterInstance.

将原始依赖项提取到参数对象中应该是您优于前面讨论的选项的首选,因为参数对象在功能和技术级别上都消除了歧义。但是,它确实需要更改组件的构造函数,这可能并不总是可行的。在这种情况下,注册代理人是您的第二好选择。

Extracting primitive Dependencies into Parameter Objects should be your preference over the previously discussed option, because Parameter Objects remove ambiguity, at both the functional and technical levels. It does, however, require a change to a component’s constructor, which might not always be feasible. In this case, registering a delegate is your second-best pick.

14.3.3 用代码块注册对象

14.3.3 Registering objects with code blocks

正如我们在上一节中讨论的那样,创建具有原始值的组件的选项之一是使用该Register方法。这使您可以提供创建组件的委托。再次注册:

As we discussed in the previous section, one of the options for creating a component with a primitive value is to use the Register method. This lets you supply a delegate that creates the component. Here’s that registration again:

container.Register<ICourse>(() => new ChiliConCarne(Spiciness.Hot));

每次解析服务时都会ChiliConCarne调用构造函数。但是,您可以使用代码块自己编写构造函数调用,而不是使用简单注入器来计算构造函数参数。Hot SpicinessICourse

The ChiliConCarne constructor is invoked with Hot Spiciness every time the ICourse service is resolved. Instead of Simple Injector figuring out the constructor arguments, however, you write the constructor invocation yourself using a code block.

当谈到应用程序类时,您通常可以在自动装配或使用代码块之间做出选择。但其他类的限制更为严格:它们不能通过公共构造函数实例化。相反,您必须使用某种工厂来创建该类型的实例。这对DI 容器来说总是很麻烦,因为默认情况下,它们会处理公共构造函数。

When it comes to application classes, you typically have a choice between Auto-Wiring or using a code block. But other classes are more restrictive: they can’t be instantiated through a public constructor. Instead, you must use some sort of factory to create instances of the type. This is always troublesome for DI Containers because, by default, they look after public constructors.

考虑公共JunkFood类的这个示例构造函数:

Consider this example constructor for the public JunkFood class:

internal JunkFood(string name)

尽管JunkFood该类可能是公共的,但构造函数是内部的。在下一个示例中,JunkFood应该通过静态JunkFoodFactory类创建实例:

Even though the JunkFood class might be public, the constructor is internal. In the next example, instances of JunkFood should instead be created through the static JunkFoodFactory class:

public static class JunkFoodFactory
{
    public static JunkFood Create(string name)
    {
        return new JunkFood(name);
    }
}

从 Simple Injector 的角度来看,这是一个有问题的 API,因为围绕静态工厂没有明确且完善的约定。它需要帮助——您可以通过提供它可以执行以创建实例的代码块来提供帮助:

From Simple Injector’s perspective, this is a problematic API, because there are no unambiguous and well-established conventions around static factories. It needs help — and you can give that help by providing a code block it can execute to create the instance:

container.Register<IMeal>(() => JunkFoodFactory.Create("chicken meal"));

这一次,您使用该Register方法通过在代码块中调用静态工厂来创建组件。JunkFoodFactory.Create每次IMeal解析都会调用,并返回结果。

This time, you use the Register method to create the component by invoking a static factory within the code block. JunkFoodFactory.Create is invoked every time IMeal is resolved, and the result is returned.

当您最终编写代码来创建实例时,这与直接调用代码有何不同?Register通过在方法调用中使用代码块,您仍然可以获得一些东西:

When you end up writing the code to create the instance, how is this in any way better than invoking the code directly? By using a code block inside a Register method call, you still gain something:

  • 你映射从IMealJunkFood这允许消费类保持松散耦合。
  • You map from IMeal to JunkFood. This allows consuming classes to stay loosely coupled.
  • 您仍然可以配置Lifestyles虽然会调用代码块来创建实例,但可能不会在每次请求实例时都调用它。它是默认设置,但如果将其更改为Singleton,代码块将只被调用一次,结果会被缓存并在之后重用。
  • You can still configure Lifestyles. Although the code block will be invoked to create the instance, it may not be invoked every time the instance is requested. It is by default, but if you change it to a Singleton, the code block will only be invoked once, and the result cached and reused thereafter.

在本节中,您了解了如何使用 Simple Injector 来处理更困难的 API。您可以将该Register方法与代码块一起使用,以获得类型更安全的方法。我们还没有研究如何使用多个组件,所以现在让我们把注意力转向那个方向。

In this section, you’ve seen how you can use Simple Injector to deal with more-difficult APIs. You can use the Register method with a code block for a more type-safe approach. We have yet to look at how to work with multiple components, so let’s now turn our attention in that direction.

14.4 使用多个组件

14.4 Working with multiple components

正如在 12.1.2 节中提到的,DI 容器在独特性上茁壮成长,但在模棱两可的情况下却很难。使用Constructor Injection时,单个构造函数优于重载构造函数,因为在别无选择时使用哪个构造函数是显而易见的。从抽象映射到具体类型时也是如此。如果您试图将多个具体类型映射到同一个抽象,就会引入歧义。

As alluded to in section 12.1.2, DI Containers thrive on distinctness but have a hard time with ambiguity. When using Constructor Injection, a single constructor is preferred over overloaded constructors, because it’s evident which constructor to use when there’s no choice. This is also the case when mapping from Abstractions to concrete types. If you attempt to map multiple concrete types to the same Abstraction, you introduce ambiguity.

尽管模棱两可的性质不受欢迎,但您经常需要处理单个抽象的多个实现。8  在这些情况下可能会出现这种情况:

Despite the undesirable qualities of ambiguity, you often need to work with multiple implementations of a single Abstraction.8  This can be the case in these situations:

  • 不同的混凝土类型用于不同的消费者。
  • Different concrete types are used for different consumers.
  • 依赖关系是序列。
  • Dependencies are sequences.
  • 正在使用装饰器或复合材料。
  • Decorators or Composites are in use.

在本节中,我们将查看这些案例中的每一个,并了解 Simple Injector 如何依次处理每个案例。当我们完成后,您应该能够注册和解析组件,即使同一抽象的多个实现正在运行。让我们首先看看在歧义的情况下如何提供细粒度的控制。

In this section, we’ll look at each of these cases and see how Simple Injector addresses each one in turn. When we’re done, you should be able to register and resolve components even when multiple implementations of the same Abstraction are in play. Let’s first see how you can provide fine-grained control in the case of ambiguity.

14.4.1 在多个候选人中选择

14.4.1 Selecting among multiple candidates

Auto-Wiring方便且功能强大,但提供的控制很少。只要所有抽象明确映射到具体类型,就没有问题。但是,一旦您引入了同一接口的更多实现,歧义就会浮出水面。让我们首先回顾一下 Simple Injector 如何处理同一个Abstraction的多个注册。

Auto-Wiring is convenient and powerful but provides little control. As long as all Abstractions are distinctly mapped to concrete types, you have no problems. But as soon as you introduce more implementations of the same interface, ambiguity rears its ugly head. Let’s first recap how Simple Injector deals with multiple registrations of the same Abstraction.

配置同一服务的多个实现

Configuring multiple implementations of the same service

正如您在 14.1.2 节中看到的,您可以像这样注册同一接口的多个实现:

As you saw in section 14.1.2, you can register multiple implementations of the same interface like this:

container.Collection.Register<IIngredient>(
    typeof(SauceBéarnaise),
    typeof(Steak));

此示例将SteakSauceBéarnaise类注册为一系列IIngredient服务。您可以要求容器解析所有IIngredient组件。Simple Injector 有一个专门的方法来做到这一点:GetAllInstances获取IEnumerable所有注册成分。这是一个例子:

This example registers both the Steak and SauceBéarnaise classes as a sequence of IIngredient services. You can ask the container to resolve all IIngredient components. Simple Injector has a dedicated method to do that: GetAllInstances gets an IEnumerable with all registered ingredients. Here’s an example:

IEnumerable<IIngredient> ingredients =
    container.GetAllInstances<IIngredient>();

您还可以要求容器IIngredient使用以下方法解析所有组件GetInstance

You can also ask the container to resolve all IIngredient components using GetInstance instead:

IEnumerable<IIngredient> ingredients =
    container.GetInstance<IEnumerable<IIngredient>>();

请注意,您请求IEnumerable<IIngredient>,但您使用的是普通GetInstance方法。Simple Injector 将此解释为约定,并为您提供IIngredient它拥有的所有组件。

Notice that you request IEnumerable<IIngredient>, but you use the normal GetInstance method. Simple Injector interprets this as a convention and gives you all the IIngredient components it has.

当某个抽象有多个实现时,通常会有一个消费者依赖于一个序列。然而,有时组件需要与固定集合或同一抽象的依赖项的子集一起工作,这就是我们接下来要讨论的内容。

When there are multiple implementations of a certain Abstraction, there’ll often be a consumer that depends on a sequence. Sometimes, however, components need to work with a fixed set or a subset of Dependencies of the same Abstraction, which is what we’ll discuss next.

使用条件注册消除歧义

Removing ambiguity using conditional registrations

与自动装配一样有用,有时您需要覆盖正常行为以提供对哪些依赖项去往何处的细粒度控制,但也可能是您需要解决不明确的 API。例如,考虑这个构造函数:

As useful as Auto-Wiring is, sometimes you need to override the normal behavior to provide fine-grained control over which Dependencies go where, but it may also be that you need to address an ambiguous API. As an example, consider this constructor:

public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)

在这种情况下,您有三个相同类型的Dependencies,每个代表一个不同的概念。在大多数情况下,您希望将每个依赖项映射到一个单独的类型。对于大多数DI 容器,此类问题的典型解决方案是使用键控或命名注册,就像您在上一章中看到的 Autofac 一样。使用 Simple Injector,解决方案通常是更改依赖项而不是消费者的注册。以下清单显示了您可以如何选择注册ICourse映射。

In this case, you have three identically typed Dependencies, each of which represents a different concept. In most cases, you want to map each of the Dependencies to a separate type. With most DI Containers, the typical solution for this type of problem is to use keyed or named registrations, as you saw with Autofac in the previous chapter. With Simple Injector, the solution is typically to change the registration of the Dependency instead of the consumer. The following listing shows how you could choose to register the ICourse mappings.

清单 14.9 根据构造函数的参数名称注册课程

Listing 14.9 Registering courses based on the constructor’s parameter names

container.Register<IMeal, ThreeCourseMeal>();    ①  

container.RegisterConditional<ICourse, Rillettes>(    ②  
    c => c.Consumer.Target.Name == "entrée");    ②  
    ②  
container.RegisterConditional<ICourse, CordonBleu>(    ②  
    c => c.Consumer.Target.Name == "mainCourse");    ②  
    ②  
container    ②  
    .RegisterConditional<ICourse, MousseAuChocolat>(  ②  
    c => c.Consumer.Target.Name == "dessert");    ②  

让我们仔细看看这里发生了什么。RegisterConditional方法_接受一个Predicate<PredicateContext>值,该值允许它确定是否应将注册注入消费者。它具有以下签名:

Let’s take a closer look at what’s going on here. The RegisterConditional method accepts a Predicate<PredicateContext> value, which allows it to determine whether a registration should be injected into the consumer or not. It has the following signature:

public void RegisterConditional<TService, TImplementation>(
    Predicate<PredicateContext> predicate)
    where TImplementation : class, TService
    where TService : class;

System.Predicate<T>是 .NET 委托类型。predicate价值_将由 Simple Injector 调用。如果predicate返回true,它使用给定消费者的注册。否则,Simple Injector 期望另一个条件注册有一个返回的委托true。当找不到注册时,它会抛出异常,因为在那种情况下,无法构建对象图。同样,当有多个适用的注册时,它会抛出异常。

System.Predicate<T> is a .NET delegate type. The predicate value will be invoked by Simple Injector. If predicate returns true, it uses the registration for the given consumer. Otherwise, Simple Injector expects another conditional registration to have a delegate that returns true. It throws an exception when it can’t find a registration, because, in that case, the object graph can’t be constructed. Likewise, it throws an exception when there are multiple registrations that are applicable.

Simple Injector 是严格的,从不假设知道你打算选择什么,正如我们之前讨论的关于具有多个构造函数的组件。不过,这确实意味着 Simple Injector 总是调用所有适用条件注册的所有谓词来查找可能的重叠注册。这可能看起来效率低下,但这些谓词仅在第一次解析组件时调用。任何后续决议都包含所有可用信息,这意味着其他决议很快。

Simple Injector is strict and never assumes to know what you intended to select, as we discussed previously regarding components with multiple constructors. This does mean, though, that Simple Injector always calls all predicates of all applicable conditional registrations to find possible overlapping registrations. This might seem inefficient, but those predicates are only called when a component is resolved for the first time. Any following resolution has all the information available, which means additional resolutions are fast.

通过使用条件注册组件覆盖自动装配,您允许简单注入器构建整个对象图,而无需恢复注册代码块,正如我们在 14.3.3 节中讨论的那样。由于前面讨论的诊断功能,这在使用 Simple Injector 时很有用。使用代码块会使容器蒙蔽,这可能会导致配置错误长时间未被发现。

By overriding Auto-Wiring using conditional registered components, you allow Simple Injector to build the entire object graph without having to revert to registering a code block, as we discussed in section 14.3.3. This is useful when working with Simple Injector because of the previously discussed diagnostic capabilities. The use of code blocks blinds a container, which might cause configuration mistakes to stay undetected for too long.

在下一节中,您将看到如何使用更明确、更灵活的方法,允许在一餐中提供任意数量的课程。为此,您必须了解 Simple Injector 如何处理列表和序列。

In the next section, you’ll see how to use the less ambiguous and more flexible approach where you allow any number of courses in a meal. To this end, you must learn how Simple Injector deals with lists and sequences.

14.4.2 接线顺序

14.4.2 Wiring sequences

在 6.1.1 节中,我们讨论了构造函数注入如何作为单一职责原则违规的警告系统。当时的教训是,与其将构造函数过度注入视为构造函数注入模式的弱点,不如庆幸它使有问题的设计如此明显。

In section 6.1.1, we discussed how Constructor Injection acts as a warning system for Single Responsibility Principle violations. The lesson then was that instead of viewing Constructor Over-injection as a weakness of the Constructor Injection pattern, you should rather rejoice that it makes a problematic design so obvious.

当谈到DI 容器和歧义时,我们看到了类似的关系。DI 容器通常不会以优雅的方式处理歧义。虽然你可以制作一个像 Simple Injector 这样的好DI 容器来处理它,但它看起来很尴尬。这通常表明您可以改进代码的设计。

When it comes to DI Containers and ambiguity, we see a similar relationship. DI Containers generally don’t deal with ambiguity in a graceful manner. Although you can make a good DI Container like Simple Injector deal with it, it can seem awkward. This is often an indication that you could improve the design of your code.

不要觉得受 Simple Injector 的束缚,您应该接受它的约定,让它引导您实现更好、更一致的设计。在本节中,我们将查看一个示例,该示例演示如何通过重构消除歧义,并展示 Simple Injector 如何处理序列。

Instead of feeling constrained by Simple Injector, you should embrace its conventions and let it guide you toward a better and more consistent design. In this section, we’ll look at an example that demonstrates how you can refactor away from ambiguity, as well as show how Simple Injector deals with sequences.

通过消除歧义重构更好的课程

Refactoring to a better course by removing ambiguity

在 14.4.1 节中,您看到了ThreeCourseMeal及其固有的歧义如何迫使您使注册复杂化。这应该会促使您重新考虑 API 设计。一个简单的概括转向IMeal采用任意数量的ICourse实例而不是恰好三个实例的实现,就像ThreeCourseMeal类的情况一样:

In section 14.4.1, you saw how the ThreeCourseMeal and its inherent ambiguity forced you to complicate your registration. This should prompt you to reconsider the API design. A simple generalization moves toward an implementation of IMeal that takes an arbitrary number of ICourse instances instead of exactly three, as was the case with the ThreeCourseMeal class:

public Meal(IEnumerable<ICourse> courses)

请注意,构造函数中不需要三个不同的实例,实例上ICourse的单个依赖IEnumerable<ICourse>项允许您向类提供任意数量的课程Meal——从零到……很多!这解决了含糊不清的问题,因为现在只有一个Dependency。此外,它还通过提供一个单一的通用类来改进 API 和实现,该类可以模拟不同类型的膳食:从只有一道菜的简单膳食到精心制作的 12 道菜晚餐。

Notice that, instead of requiring three distinct ICourse instances in the constructor, the single Dependency on an IEnumerable<ICourse> instance lets you provide any number of courses to the Meal class — from zero to ... a lot! This solves the issue with ambiguity, because there’s now only a single Dependency. In addition, it also improves the API and implementation by providing a single, general-purpose class that can model different types of meal: from a simple meal with a single course to an elaborate 12-course dinner.

在本节中,我们将了解如何配置 Simple Injector 来连接具有适当DependenciesMeal的实例。完成后,您应该对需要使用Dependencies序列配置实例时可用的选项有一个很好的了解。ICourse

In this section, we’ll look at how you can configure Simple Injector to wire up Meal instances with appropriate ICourse Dependencies. When you’re done, you should have a good idea of the options available when you need to configure instances with sequences of Dependencies.

自动接线序列

Auto-Wiring sequences

Simple Injector 对序列有很好的理解,所以如果你想使用给定服务的所有注册组件,自动装配就可以了。例如,给定一组已配置的ICourse实例,您可以IMeal像这样配置服务:

Simple Injector has a good understanding of sequences, so if you want to use all registered components of a given service, Auto-Wiring just works. As an example, given a set of configured ICourse instances, you can configure the IMeal service like this:

container.Register<IMeal, Meal>();

请注意,这是从抽象到具体类型的完全标准映射。Simple Injector 自动理解Meal构造函数并确定正确的操作过程是解析所有ICourse组件。当您 resolve 时IMeal,您将获得一个包含组件的Meal实例。ICourse这仍然需要您注册ICourse组件序列,例如,使用自动注册

Notice that this is a completely standard mapping from an Abstraction to a concrete type. Simple Injector automatically understands the Meal constructor and determines that the correct course of action is to resolve all ICourse components. When you resolve IMeal, you get a Meal instance with the ICourse components. This still requires you to register the sequence of ICourse components, for instance, using Auto-Registration:

container.Collection.Register<ICourse>(assembly);

Simple Injector 自动处理序列,除非您另外指定,否则它会执行您期望它做的事情:它为该Abstraction的所有注册解析一系列依赖关系。只有当您需要明确地只从更大的集合中挑选一些组件时,您才需要做更多的事情。让我们看看如何做到这一点。

Simple Injector automatically handles sequences, and unless you specify otherwise, it does what you’d expect it to do: it resolves a sequence of Dependencies for all registrations of that Abstraction. Only when you need to explicitly pick only some components from a larger set do you need to do more. Let’s see how you can do that.

从更大的集合中只挑选一些组件

Picking only some components from a larger set

Simple Injector 注入所有组件的默认策略通常是正确的策略,但如图 14.5所示,可能存在您只想从所有已注册组件的较大集合中挑选一些已注册组件的情况。

Simple Injector’s default strategy of injecting all components is often the correct policy, but as figure 14.5 shows, there may be cases where you want to pick only some registered components from the larger set of all registered components.

14-05.eps

图 14.5 从更大的所有已注册组件集合中挑选组件

Figure 14.5 Picking components from a larger set of all registered components

当你之前让Simple Injector Auto-RegisterAuto-Wire所有配置好的实例时,就对应了图中右边描述的情况。如果你想注册一个组件,如左侧所示,你必须明确定义应该使用哪些组件。为了实现这一点,您可以使用Collection.Create允许创建序列子集的方法。以下清单显示了如何将序列的子集注入消费者。

When you previously let Simple Injector Auto-Register and Auto-Wire all configured instances, it corresponded to the situation depicted on the right side of the figure. If you want to register a component as shown on the left side, you must explicitly define which components should be used. In order to achieve this, you can use the Collection.Create method, which allows creating a subset of a sequence. The following listing shows how to inject a subset of a sequence into a consumer.

清单 14.10 将序列子集注入消费者

Listing 14.10 Injecting a sequence subset into a consumer

IEnumerable<ICourse> coursesSubset1 =
    container.Collection.Create<ICourse>(    ①  
        typeof(Rillettes),    ①  
        typeof(CordonBleu),    ①  
        typeof(MousseAuChocolat));    ①  

IEnumerable<ICourse> coursesSubset2 =
    container.Collection.Create<ICourse>(    ②  
        typeof(CeasarSalad),    ②  
        typeof(ChiliConCarne),    ②  
        typeof(MousseAuChocolat));    ②  

container.RegisterInstance<IMeal>(    ③  
    new Meal(sourcesSubset1));    ③  

Collection.Create方法_允许您创建给定抽象的序列。序列本身不会在容器中注册——这可以使用Collection.Register. Collection.Create通过多次调用同一个Abstraction,您可以创建多个序列,它们都是不同的子集,如清单 14.10所示。

The Collection.Create method lets you create a sequence of a given Abstraction. The sequence itself won’t be registered in the container — this can be done using Collection.Register. By calling Collection.Create multiple times for the same Abstraction, you can create multiple sequences that are all different subsets, as shown in listing 14.10.

清单 14.10可能令人惊讶的是调用Collection.Create并没有在那个时间点创建课程。相反,序列是一个流。只有当您开始迭代序列时,它才会开始解析实例。由于这种行为,序列子集可以安全地注入到Singleton Meal中而不会造成任何伤害。我们将在 14.4.5 节中更详细地介绍流。

What might be surprising about listing 14.10 is that the call to Collection.Create doesn’t create the courses at that point in time. Instead, the sequence is a stream. Only when you start iterating the sequence will it start to resolve instances. Because of this behavior, the sequence subset can be safely injected into the Singleton Meal without causing any harm. We’ll go into more detail about streams in section 14.4.5.

Simple Injector 本机理解序列。除非您需要从给定类型的所有服务中明确地只选择一些组件,否则 Simple Injector 会自动做正确的事情。

Simple Injector natively understands sequences. Unless you need to explicitly pick only some components from all services of a given type, Simple Injector automatically does the right thing.

Auto-Wiring不仅适用于单个实例,也适用于序列;容器将序列映射到相应类型的所有已配置实例。具有相同抽象的多个实例的一种不太直观的用法是装饰器设计模式,我们将在接下来讨论。

Auto-Wiring works not only with single instances, but also for sequences; the container maps a sequence to all configured instances of the corresponding type. A perhaps less intuitive use of having multiple instances of the same Abstraction is the Decorator design pattern, which we’ll discuss next.

14.4.3 接线装饰器

14.4.3 Wiring Decorators

在 9.1.1 节中,我们讨论了装饰器设计模式在实现横切关注点时如何发挥作用。根据定义,装饰器引入了相同抽象的多种类型。至少,您有两个抽象实现:装饰器本身和装饰类型。如果你堆叠装饰器,你可以拥有更多。这是对同一服务进行多次注册的另一个示例。与前面的部分不同,这些注册在概念上并不相等,而是彼此的依赖关系。

In section 9.1.1, we discussed how the Decorator design pattern is useful when implementing Cross-Cutting Concerns. By definition, Decorators introduce multiple types of the same Abstraction. At the very least, you have two implementations of an Abstraction: the Decorator itself and the decorated type. If you stack the Decorators, you can have even more. This is another example of having multiple registrations of the same service. Unlike the previous sections, these registrations aren’t conceptually equal, but rather Dependencies of each other.

Simple Injector 内置支持使用该RegisterDecorator方法注册装饰器。并且,在本节中,我们将讨论非通用和通用抽象的注册。让我们从前者开始。

Simple Injector has built-in support for registering Decorators using the RegisterDecorator method. And, in this section, we’ll discuss both registrations of non-generic and generic Abstractions. Let’s start with the former.

装饰非泛型抽象

Decorating non-generic Abstractions

使用RegisterDecorator方法,你可以优雅地注册一个Decorator。以下示例显示如何使用此方法应用于Breadinga VealCutlet

Using the RegisterDecorator method, you can elegantly register a Decorator. The following example shows how to use this method to apply Breading to a VealCutlet:

var c = new Container();

c.Register<IIngredient, VealCutlet>();    ①  

c.RegisterDecorator<IIngredient, Breading>();    ②  

正如你在第 9 章中学到的,当你在小牛肉排上切开一个口袋,然后在给小牛肉排裹上面包屑之前,将火腿、奶酪和大蒜放入口袋中,你就会得到小牛肉蓝带。下面的例子展示了如何在Decorator和DecoratorHamCheeseGarlic之间添加一个Decorator:VealCutletBreading

As you learned in chapter 9, you get veal cordon bleu when you slit open a pocket in the veal cutlet and add ham, cheese, and garlic into the pocket before breading the cutlet. The following example shows how to add a HamCheeseGarlic Decorator in between VealCutlet and the Breading Decorator:

var c = new Container();

c.Register<IIngredient, VealCutlet>();

c.RegisterDecorator<IIngredient, HamCheeseGarlic>();  ①  

c.RegisterDecorator<IIngredient, Breading>();

通过在注册之前放置这个新注册BreadingHamCheeseGarlicDecorator 将首先被包装。这导致对象图等于以下纯 DI版本:

By placing this new registration before the Breading registration, the HamCheeseGarlic Decorator will be wrapped first. This results in an object graph equal to the following Pure DI version:

new Breading(    ①  
    new HamCheeseGarlic(    ①  
        new VealCutlet()));    ①  

RegisterDecorator在 Simple Injector 中使用该方法链接装饰器很容易。同样,您可以应用通用装饰器,如下所示。

Chaining Decorators using the RegisterDecorator method is easy in Simple Injector. Likewise, you can apply generic Decorators, as you’ll see next.

装饰通用抽象

Decorating generic Abstractions

在第 10 章中,我们定义了多个可以应用于任何ICommandService<TCommand>实现的通用装饰器。在本章的剩余部分,我们将把成分和课程放在一边,看看如何使用 Simple Injector 注册这些通用装饰器。以下清单演示了如何ICommandService<TCommand>使用 10.3 节中介绍的三个装饰器注册所有实现。

During the course of chapter 10, we defined multiple generic Decorators that could be applied to any ICommandService<TCommand> implementation. In the remainder of this chapter, we’ll set our ingredients and courses aside, and take a look at how to register these generic Decorators using Simple Injector. The following listing demonstrates how to register all ICommandService<TCommand> implementations with the three Decorators presented in section 10.3.

清单 14.11 装饰通用的自动注册抽象

Listing 14.11 Decorating generic Auto-Registered Abstractions

container.Register(    ①  
    typeof(ICommandService<>), assembly);    ①  

container.RegisterDecorator(    ②  
    typeof(ICommandService<>),    ②  
    typeof(AuditingCommandServiceDecorator<>));    ②  
    ②  
container.RegisterDecorator(    ②  
    typeof(ICommandService<>),    ②  
    typeof(TransactionCommandServiceDecorator<>));  ②  
    ②  
container.RegisterDecorator(    ②  
    typeof(ICommandService<>),    ②  
    typeof(SecureCommandServiceDecorator<>));    ②  

清单 14.3 所示,您使用重载通过扫描程序集Register来注册任意实现。ICommandService<TCommand>要注册通用装饰器,您可以使用RegisterDecorator接受两个Type实例的方法。清单 14.11的配置结果是图 14.6,我们之前在 10.3.4 节中讨论过。

As in listing 14.3, you use a Register overload to register arbitrary ICommandService<TCommand> implementations by scanning assemblies. To register generic Decorators, you use the RegisterDecorator method that accepts two Type instances. The result of the configuration of listing 14.11 is figure 14.6, which we discussed previously in section 10.3.4.

14-06.eps

图 14.6 用事务、审计和安全方面丰富一个真正的命令服务

Figure 14.6 Enriching a real command service with transaction, auditing, and security aspects

说到Simple Injector对Decorators的支持,这只是冰山一角。几个RegisterDecorator重载允许有条件地创建装饰器,就像前面讨论的清单 14.9RegisterConditional的重载一样。然而,对这一特性和其他特性的讨论超出了本书的范围。9 

When it comes to Simple Injector’s support for Decorators, this is only the tip of the iceberg. Several RegisterDecorator overloads allow Decorators to be made conditionally, like the previously discussed RegisterConditional overload of listing 14.9. A discussion of this and other features, however, is out of the scope of this book.9 

Simple Injector 允许您以多种不同的方式使用多个 Decorator 实例。您可以将组件注册为彼此的替代项、解析为序列的对等项或分层装饰器。在许多情况下,Simple Injector 会弄清楚要做什么。如果您需要更明确的控制,您始终可以明确定义服务的组成方式。

Simple Injector lets you work with multiple Decorator instances in several different ways. You can register components as alternatives to each other, as peers resolved as sequences, or as hierarchical Decorators. In many cases, Simple Injector figures out what to do. You can always explicitly define how services are composed if you need more-explicit control.

在本节中,我们重点介绍了专门为配置装饰器而设计的 Simple Injector 方法。尽管依赖依赖序列的消费者可以最直观地使用同一抽象的多个实例,但装饰器是另一个很好的例子。但是还有第三种可能有点令人惊讶的情况,其中多个实例开始发挥作用,这就是复合设计模式。

In this section, we focused on Simple Injector’s methods that were explicitly designed for configuring Decorators. Although consumers that rely on sequences of Dependencies can be the most intuitive use of multiple instances of the same Abstraction, Decorators are another good example. But there’s a third and perhaps a bit surprising case where multiple instances come into play, which is the Composite design pattern.

14.4.4 布线复合材料

14.4.4 Wiring Composites

在本书的学习过程中,我们多次讨论了复合设计模式。例如,在 6.1.2 节中,您创建了一个(清单 6.4),它实现并包装了一系列实现。CompositeNotificationServiceINotificationServiceINotificationService

During the course of this book, we discussed the Composite design pattern on several occasions. In section 6.1.2, for instance, you created a CompositeNotificationService (listing 6.4) that both implemented INotificationService and wrapped a sequence of INotificationService implementations.

布线非通用复合材料

Wiring non-generic Composites

让我们看一下如何注册 Composites,例如Simple Injector 中第 6 章的内容。下面的清单再次显示了这个类。CompositeNotificationService

Let’s take a look at how you can register Composites, such as the CompositeNotificationService from chapter 6 in Simple Injector. The following listing shows this class again.

清单 14.12来自第 6 章 的CompositeNotificationServiceComposite

Listing 14.12 The CompositeNotificationService Composite from chapter 6

public class CompositeNotificationService : INotificationService
{
    private readonly IEnumerable<INotificationService> services;

    public CompositeNotificationService(
        IEnumerable<INotificationService> services)
    {
        this.services = services;
    }

    public void OrderApproved(Order order)
    {
        foreach (INotificationService service in this.services)
        {
            service.OrderApproved(order);
        }
    }
}

由于 Simple Injector API 将序列注册与非序列注册分开,因此 Composites 的注册再简单不过了。您可以将 Composite 注册为单个注册,同时将其依赖项注册为序列:

Because the Simple Injector API separates the registration of sequences from non-sequence registrations, the registration of Composites couldn’t be any easier. You can register the Composite as a single registration, while registering its Dependencies as a sequence:

container.Collection.Register<INotificationService>(
    typeof(OrderApprovedReceiptSender),
    typeof(AccountingNotifier),
    typeof(OrderFulfillment),
);

container.Register<INotificationService, CompositeNotificationService>();

在前面的示例中,三个INotificationService实现被注册为一个序列,使用Collection.Register. CompositeNotificationService另一方面,注册为单个非序列注册。所有类型均由简单注入器自动接线。使用之前的注册,当 anINotificationService被解析时,它会产生类似于以下纯 DI表示的对象图:

In the previous example, three INotificationService implementations are registered as a sequence using Collection.Register. The CompositeNotificationService, on the other hand, is registered as single, non-sequence registration. All types are Auto-Wired by Simple Injector. Using the previous registration, when an INotificationService is resolved, it results in an object graph similar to the following Pure DI representation:

return new CompositeNotificationService(new INotificationService[]
{
    new OrderApprovedReceiptSender(),
    new AccountingNotifier(),
    new OrderFulfillment()
});

由于通知服务的数量可能会随着时间的推移而增加,您可以通过使用接受. 这使您可以将之前的类型列表变成一个简单的单行代码:Collection.RegisterAssembly

Because the number of notification services will likely grow over time, you can reduce the burden on your Composition Root by applying Auto-Registration using the Collection.Register overload that accepts an Assembly. This lets you turn the previous list of types into a simple one-liner:

container.Collection.Register<INotificationService>(assembly);

container.Register<INotificationService, CompositeNotificationService>();

您可能还记得第 13 章中类似的构造在 Autofac 中不起作用,因为 Autofac 的自动注册会注册组合以及序列的一部分。然而,Simple Injector 并非如此。它的Collection.Register方法会自动过滤掉任何复合类型,并防止它们被注册为序列的一部分。

You may recall from chapter 13 that a similar construct in Autofac didn’t work, because Autofac’s Auto-Registration would register the Composite as well as part of the sequence. This, however, isn’t the case with Simple Injector. It’s Collection.Register method automatically filters out any Composite types and prevents them from being registered as part of the sequence.

然而,复合类并不是唯一会被 Simple Injector 自动从列表中删除的类。Simple Injector 也以相同的方式检测装饰器。这种行为使得在 Simple Injector 中使用 Decorators 和 Composites 变得轻而易举。这同样适用于使用通用复合材料。

Composite classes, however, aren’t the only classes that will automatically be removed from the list by Simple Injector. Simple Injector also detects Decorators in the same way. This behavior makes working with Decorators and Composites in Simple Injector a breeze. The same holds true for working with generic Composites.

布线通用复合材料

Wiring generic Composites

在 14.4.2 节中,您了解了 Simple Injector 的RegisterDecorator方法使注册通用装饰器看起来像儿戏。在本节中,我们将看看如何为通用抽象注册 Composites 。

In section 14.4.2, you saw how Simple Injector’s RegisterDecorator method made registering generic Decorators look like child’s play. In this section, we’ll take a look at how you can register Composites for generic Abstractions.

在 6.1.3 节中,您指定了CompositeEventHandler<TEvent>(清单 6.12)作为一系列实现的复合IEventHandler<TEvent>实现。让我们看看您是否可以使用其包装的事件处理程序实现来注册 Composite。我们将从事件处理程序的自动注册开始:

In section 6.1.3, you specified the CompositeEventHandler<TEvent> class (listing 6.12) as a Composite implementation over a sequence of IEventHandler<TEvent> implementations. Let’s see if you can register the Composite with its wrapped event handler implementations. We’ll start with the Auto-Registration of the event handlers:

container.Collection.Register(typeof(IEventHandler<>), assembly);

与清单 14.3ICommandService<T>中的实现注册相比,您现在使用instead of 。那是因为对于特定类型的事件可能有多个处理程序。这意味着您必须明确声明您知道单个事件类型会有更多实现。如果您不小心调用了而不是,Simple Injector 会抛出类似于以下的异常:Collection.RegisterRegisterRegisterCollection.Register

In contrast to the registration of ICommandService<T> implementations in listing 14.3, you now use Collection.Register instead of Register. That’s because there’ll potentially be multiple handlers for a particular type of event. This means you have to explicitly state that you know there’ll be more implementations for the single event type. Were you to have accidentally called Register instead of Collection.Register, Simple Injector would have thrown an exception similar to the following:

在提供的类型或程序集列表中,有 3 种类型表示相同的封闭泛型类型 IEventHandler<OrderApproved>。您是否打算使用 Collection.Register 方法将类型注册为集合?冲突类型:OrderApprovedReceiptSender、AccountingNotifier 和 OrderFulfillment。

In the supplied list of types or assemblies, there are 3 types that represent the same closed-generic type IEventHandler<OrderApproved>. Did you mean to register the types as a collection using the Collection.Register method instead? Conflicting types: OrderApprovedReceiptSender, AccountingNotifier, and OrderFulfillment.

此消息的一个好处是它已经表明您很可能应该使用Collection.Register而不是Register. 但也有可能是您不小心添加了一个被拾取的无效类型。正如我们之前解释的那样,当涉及到歧义时,Simple Injector 会强制您明确说明,这有助于检测错误。

A nice thing about this message is that it already indicates you most likely should be using Collection.Register instead of Register. But it’s also possible that you accidentally added an invalid type that was picked up. As we explained before, when it comes to ambiguity, Simple Injector forces you to be explicit, which is helpful in detecting errors.

剩下的就是注册CompositeEventHandler<TEvent>. 因为CompositeEventHandler<TEvent>是泛型类型,所以您必须使用Register接受Type参数的重载:

What remains is the registration for CompositeEventHandler<TEvent>. Because CompositeEventHandler<TEvent> is a generic type, you’ll have to use the Register overload that accepts Type arguments:

container.Register(    ①  
    typeof(IEventHandler<>),    ①  
    typeof(CompositeEventHandler<>));  ①  

使用此注册,当请求特定的封闭IEventHandler<TEvent> 抽象时(例如,IEventHandler<OrderApproved>),简单注入器确定要创建的确切CompositeEventHandler<TEvent>类型。在这种情况下,这是相当简单的,因为请求IEventHandler<OrderApproved>结果会CompositeEventHandler<OrderApproved>得到解决。在其他情况下,确定确切的封闭类型可能是一个相当复杂的过程,但 Simple Injector 可以很好地处理这个问题。

Using this registration, when a particular closed IEventHandler<TEvent> Abstraction is requested (for example, IEventHandler<OrderApproved>), Simple Injector determines the exact CompositeEventHandler<TEvent> type to create. In this case, this is rather straightforward, because requesting an IEventHandler<OrderApproved> results in a CompositeEventHandler<OrderApproved> getting resolved. In other cases, determining the exact closed type can be a rather complex process, but Simple Injector handles this well.

在 Simple Injector 中使用序列非常简单。然而,在解析和注入序列时,Simple Injector 与其他DI 容器相比,以一种迷人的方式表现不同。正如我们之前提到的,Simple Injector 将序列作为流处理。

Working with sequences is rather straightforward in Simple Injector. When it comes to resolving and injecting sequences, however, Simple Injector behaves differently compared to other DI Containers in a captivating way. As we alluded earlier, Simple Injector handles sequences as streams.

14.4.5 序列是流

14.4.5 Sequences are streams

在第 14.1 节中,您注册了一系列成分,如下所示:

In section 14.1, you registered a sequence of ingredients as follows:

container.Collection.Register<IIngredient>(
    typeof(SauceBéarnaise),
    typeof(Steak));

如前所示,您可以要求容器IIngredient使用或方法解析所有组件。这是再次使用的示例:GetAllInstancesGetInstanceGetInstance

As shown previously, you can ask the container to resolve all IIngredient components using either the GetAllInstances or GetInstance methods. Here’s the example using GetInstance again:

IEnumerable<IIngredient> ingredients =
    container.GetInstance<IEnumerable<IIngredient>>();

您可能希望调用 来GetInstance<IEnumerable<IIngredient>>()创建两个类的实例,但这与事实相差甚远。解析或注入 时IEnumerable<T>,Simple Injector 不会立即用所有成分预填充序列。相反,IEnumerable<T>表现得像一个流。10  这意味着返回IEnumerable<IIngredient>的是一个能够在IIngredient迭代时生成新实例的对象。这类似于使用 a 从磁盘流式传输数据System.IO.FileStream或使用 a 从数据库流式传输数据,其中数据以小块的形式到达,而不是一次性预取所有数据。System.Data.SqlClient.SqlDataReader

You might expect the call to GetInstance<IEnumerable<IIngredient>>() to create an instance of both classes, but this couldn’t be further from the truth. When resolving or injecting an IEnumerable<T>, Simple Injector doesn’t prepopulate the sequence with all ingredients right away. Instead, IEnumerable<T> behaves like a stream.10  What this means is that the returned IEnumerable<IIngredient> is an object that’s able to produce new IIngredient instances when it’s iterated. This is similar to streaming data from disk using a System.IO.FileStream or a database using a System.Data.SqlClient.SqlDataReader, where data arrives in small chunks rather than prefetching all the data in one go.

以下示例显示了多次迭代流如何生成新实例:

The following example shows how iterating a stream multiple times can produce new instances:

IEnumerable<IIngredient> stream =
    container.GetAllInstance<IIngredient>();

IIngredient ingredient1 = stream.First();    ①  
IIngredient ingredient2 = stream.First();    ②  

object.ReferenceEquals(ingredient1, ingredient2);  ③  

当流被迭代时,它会回调到容器中,以根据其适当的Lifestyle解析序列的元素。这意味着如果类型注册为Transient,则始终会生成新实例,如前一个示例所示。但是,当类型为Singleton时,每次都会返回相同的实例:

When a stream is iterated, it calls back into the container to resolve elements of the sequence based on their appropriate Lifestyle. This means that if the type is registered as Transient, new instances are always produced, as the previous example showed. When the type is Singleton, however, the same instance is returned every time:

var c = new Container();

c.Collection.Append<IIngredient, SauceBéarnaise>();    ①  
c.Collection.Append<IIngredient, Steak>(    ①  
    Lifestyle.Singleton);    ①  

var s = c.GetInstance<IEnumerable<IIngredient>>();

object.ReferenceEquals(s.First(), s.First());    ②  
object.ReferenceEquals(s.Last(), s.Last());    ③  

尽管流式传输不是DI 容器下的常见特征,但它有一些有趣的优点。首先,当将流注入消费者时,流本身的注入实际上是免费的,因为在那个时间点没有创建实例。11  这在元素列表很大并且在消费者的生命周期内不需要所有元素时很有用。以下面的 CompositeILogger实现为例。它是清单 8.22 中 Composite 的变体,但在本例中,Composite 在其中一个包装记录器成功后直接停止记录。

Although streaming isn’t a common trait under DI Containers, it has a few interesting advantages. First, when injecting a stream into a consumer, the injection of the stream itself is practically free, because no instance is created at that point in time.11  This is useful when the list of elements is big, and not all elements are needed during the lifetime of the consumer. Take the following Composite ILogger implementation, for instance. It’s a variation of the Composite of listing 8.22 but, in this case, the Composite stops logging directly after one of the wrapped loggers succeeds.

清单 14.13 处理部分注入流的 Composite

Listing 14.13 A Composite that processes part of the injected stream

public class CompositeLogger : ILogger    ①  
{
    private readonly IEnumerable<ILogger> loggers;

    public CompositeLogger(
        IEnumerable<ILogger> loggers)    ②  
    {
        this.loggers = loggers;
    }

    public void Log(LogEntry entry)
    {
        foreach (ILogger logger in this.loggers)    ③  
        {
            try
            {
                logger.Log(entry);
                break;    ④  
            }
            catch { }    ⑤  
        }
    }
}

正如您在 14.4.4 节中看到的,您可以按如下方式注册CompositeLogger和实现序列:ILogger

As you saw in section 14.4.4, you can register the CompositeLogger and the sequence of ILogger implementations as follows:

container.Collection.Register<ILogger>(assembly);
container.Register<ILogger, CompositeLogger>(Lifestyle.Singleton);

在本例中,您将 the 注册CompositeLoggerSingleton,因为它是无状态的,并且它唯一的依赖项theIEnumerable<ILogger>本身就是一个SingletonCompositeLoggerILogger序列作为单例的效果是注入CompositeLogger实际上是免费的。即使当消费者调用其DependencyLog方法时,这通常只会导致创建ILogger序列的第一个实现——而不是所有的实现。

In this case, you registered the CompositeLogger as Singleton because it’s stateless, and its only Dependency, the IEnumerable<ILogger>, is itself a Singleton. The effect of the CompositeLogger and ILogger sequences as Singletons is that the injecting of CompositeLogger is practically free. Even when a consumer calls its Dependency’s Log method, this typically only results in the creation of the first ILogger implementation of the sequence — not all of them.

序列作为流的第二个优点是,只要您只存储对 的引用IEnumerable<ILogger>,如清单 14.13所示,序列的元素永远不会意外地变成Captive Dependencies。前面的例子已经说明了这一点。Singleton CompositeLogger可以安全地依赖,IEnumerable<ILogger>因为它也是一个Singleton,即使它生产的服务可能不是。

A second advantage of sequences being streams is that, as long as you only store the reference to IEnumerable<ILogger>, as listing 14.13 showed, the sequence’s elements can never accidentally become Captive Dependencies. The previous example already showed this. The Singleton CompositeLogger could safely depend on IEnumerable<ILogger>, because it also is a Singleton, even though its produced services might not be.

在本节中,您了解了如何处理多个组件,例如序列、装饰器和组合。我们对 Simple Injector 的讨论到此结束。在下一章中,我们将把注意力转向 Microsoft.Extensions.DependencyInjection。

In this section, you’ve seen how to deal with multiple components such as sequences, Decorators, and Composites. This ends our discussion of Simple Injector. In the next chapter, we’ll turn our attention to Microsoft.Extensions.DependencyInjection.

概括

Summary

  • Simple Injector 是一个现代的DI 容器,提供了相当全面的功能集,但它的 API 与大多数DI 容器有很大不同。以下是它的一些特征属性:
    • 范围是环境的。
    • 序列是使用Collection.Register而不是附加相同抽象的新注册来注册的。
    • 序列表现为流。
    • 可以诊断容器以发现常见的配置陷阱。
  • Simple Injector is a modern DI Container that offers a fairly comprehensive feature set, but its API is quite different from most DI Containers. The following are a few of its characteristic attributes:
    • Scopes are ambient.
    • Sequences are registered using Collection.Register instead of appending new registrations of the same Abstraction.
    • Sequences behave as streams.
    • The container can be diagnosed to find common configuration pitfalls.
  • Simple Injector 的一个重要整体主题是严格性。它不会尝试猜测您的意思,而是会尝试通过其 API 和诊断工具来防止和检测配置错误。
  • An important overall theme for Simple Injector is one of strictness. It doesn’t attempt to guess what you mean and tries to prevent and detect configuration errors through its API and diagnostic facility.
  • Simple Injector 强制执行注册和解析的严格分离。尽管您Container对注册和解析使用相同的实例,但Container在第一次使用后会被锁定。
  • Simple Injector enforces a strict separation of registration and resolution. Although you use the same Container instance for both register and resolve, the Container is locked after first use.
  • 由于 Simple Injector 的环境作用域,直接从根容器解析是一种很好的做法并受到鼓励:它不会导致内存泄漏或并发错误。
  • Because of Simple Injector’s ambient scopes, resolving from the root container directly is good practice and encouraged: it doesn’t lead to memory leaks or concurrency bugs.
  • Simple Injector 支持标准的LifestylesTransientSingletonScoped
  • Simple Injector supports the standard Lifestyles: Transient, Singleton, and Scoped.
  • Simple Injector 对序列、装饰器、合成器和泛型的注册有很好的支持。
  • Simple Injector has excellent support for registration of sequences, Decorators, Composites, and generics.

15

Microsoft.Extensions.DependencyInjection DI 容器

15

The Microsoft.Extensions.DependencyInjection DI Container

在这一章当中

In this chapter

  • 使用 Microsoft.Extensions.DependencyInjection 的注册 API
  • Working with Microsoft.Extensions.DependencyInjection’s registration API
  • 管理组件生命周期
  • Managing component lifetime
  • 配置困难的 API
  • Configuring difficult APIs
  • 配置序列、装饰器和组合
  • Configuring sequences, Decorators, and Composites

随着 ASP.NET Core 的推出,微软引入了自己的DI 容器Microsoft.Extensions.DependencyInjection,作为 Core 框架的一部分。在本章中,我们将该名称简称为MS.DI。

With the introduction of ASP.NET Core, Microsoft introduced its own DI Container, Microsoft.Extensions.DependencyInjection, as part of the Core framework. In this chapter, we shorten that name to MS.DI.

Microsoft 构建 MS.DI 是为了简化使用 ASP.NET Core 的框架和第三方组件开发人员的依赖关系管理。Microsoft 的意图是定义一个DI 容器,它具有所有其他DI 容器都可以遵循的最小的、最低的公分母功能集。

Microsoft built MS.DI to simplify Dependency management for framework and third-party component developers working with ASP.NET Core. Microsoft’s intention was to define a DI Container with a minimal, lowest common denominator feature set that all other DI Containers could conform to.

在本章中,我们将对 MS.DI 进行与对 Autofac 和 Simple Injector 相同的处理。您将看到 MS.DI 可以在多大程度上应用第 1-3 部分中提出的原则和模式。尽管 MS.DI 集成在 ASP.NET Core 中,但它也可以单独使用,这就是为什么在本章中我们将其作为单独使用。

In this chapter, we’ll give MS.DI the same treatment that we gave Autofac and Simple Injector. You’ll see to which degree MS.DI can be used to apply the principles and patterns laid forth in parts 1–3. Even though MS.DI is integrated in ASP.NET Core, it can also be used separately, which is why, in this chapter, we treat it as such.

然而,在本章的学习过程中,您会发现 MS.DI 的功能非常有限,以至于我们认为它不适合开发任何规模适中、遵循松散耦合并遵循本书描述的原则和模式的应用程序。如果 MS.DI 不适合,那么为什么要在本书中用一整章来介绍它呢?最重要的原因是 MS.DI 乍一看与其他DI Container非常相似,您需要花一些时间来了解它与成熟的DI Container之间的区别。因为它是 .NET Core 的一部分,所以如果您不了解它的局限性,可能很想使用这个内置容器。本章的目的是揭示这些局限性,以便您做出明智的决定。

During the course of this chapter, however, you’ll find that MS.DI is so limited in functionality that we deem it unsuited for development of any reasonably sized application that practices loose coupling and follows the principles and patterns described in this book. If MS.DI isn’t suited, then why use an entire chapter covering it in this book? The most important reason is that MS.DI looks at a first glance so much like the other DI Containers that you need to spend some time with it to understand the differences between it and mature DI Containers. Because it’s part of .NET Core, it may be tempting to use this built-in container if you don’t understand its limitations. The purpose of this chapter is to reveal these limitations so you can make an informed decision.

本章分为四节。您可以独立阅读每个部分,尽管第一部分是其他部分的先决条件,而第四部分依赖于第三部分介绍的一些方法和类。您可以将本章与第 4 部分的其余部分分开阅读,专门了解 MS.DI,或者您可以将其与其他章节一起阅读以比较DI 容器。本章的重点是展示 MS.DI 如何关联并实现第 1-3 部分中描述的模式和原则。

This chapter is divided into four sections. You can read each section independently, though the first section is a prerequisite for the other sections, and the fourth section relies on some methods and classes introduced in the third section. You can read the chapter in isolation from the rest of part 4, specifically to learn about MS.DI, or you can read it together with the other chapters to compare DI Containers. The focus of this chapter is to show how MS.DI relates to and implements the patterns and principles described in parts 1–3.

15.1 介绍 Microsoft.Extensions.DependencyInjection

15.1 Introducing Microsoft.Extensions.DependencyInjection

在本节中,您将了解从何处获取 MS.DI、获取的内容以及如何开始使用它。我们还将查看常见的配置选项。表 15.1提供了开始时可能需要的基本信息。

In this section, you’ll learn where to get MS.DI, what you get, and how you start using it. We’ll also look at common configuration options. Table 15.1 provides fundamental information that you’re likely to need to get started.

表 15.1 Microsoft.Extensions.DependencyInjection 一目了然
回答
我从哪里得到它?如果您创建新的 ASP.NET Core 应用程序,它会自动包含,但您也可以手动将它添加到其他应用程序类型。在 Visual Studio 中,您可以通过 NuGet 获取它。程序包名称是 Microsoft.Extensions.DependencyInjection。
支持哪些平台?.NET Standard 2.0(.NET Core 2.0、.NET Framework 4.6.1、Mono 5.4、Xamarin.iOS 10.14、Xamarin.Android 8.0、UWP 10.0.16299)。
它要多少钱?没有什么。它是开源的。
它是如何获得许可的?Apache 许可证,版本 2.0
我在哪里可以得到帮助?因为这是官方的 Microsoft .NET 产品,所以在https://www.microsoft.com/net/support/policy上有保证的商业支持。对于非商业(无保证)支持,您可能会通过在https://stackoverflow.com/上的 Stack Overflow 上寻求帮助来获得帮助。
本章基于哪个版本?2.1.0

在高层次上,使用 MS.DI 与 Autofac(在第 13 章中讨论)并没有什么不同。它的使用是一个两步过程,如图 15.1所示。然而,与 Simple Injector 相比,MS.DI 的这个两步过程是明确的:首先,您配置一个ServiceCollection,完成后,您使用它来构建一个可用于解析组件的 。ServiceProvider

At a high level, using MS.DI isn’t that different from Autofac (discussed in chapter 13). Its usage is a two-step process, as figure 15.1 illustrates. Compared to Simple Injector, however, with MS.DI this two-step process is explicit: first, you configure a ServiceCollection, and when you’re done with that, you use it to build a ServiceProvider that can be used to resolve components.

15-01.eps

图 15.1 Microsoft.Extensions.DependencyInjection 的使用模式是先配置再解析组件。

Figure 15.1 The pattern for using Microsoft.Extensions.DependencyInjection is to first configure it and then resolve components.

完成本节后,您应该对 MS.DI 的整体使用模式有一个良好的感觉,并且您应该能够在行为良好的场景中开始使用它——所有组件都遵循正确的 DI 模式,例如构造函数注入。让我们从最简单的场景开始,看看如何使用 MS.DI 容器解析对象。

When you’re done with this section, you should have a good feeling for the overall usage pattern of MS.DI, and you should be able to start using it in well-behaved scenarios — where all components follow proper DI patterns, such as Constructor Injection. Let’s start with the simplest scenario and see how you can resolve objects using an MS.DI container.

15.1.1 解析对象

15.1.1 Resolving objects

任何DI 容器的核心服务都是组合对象图。在本节中,我们将了解使您能够使用 MS.DI 编写对象图的 API。MS.DI 要求您在解析之前注册所有相关组件。下面的清单显示了 MS.DI 最简单的可能用法之一。

The core service of any DI Container is to compose object graphs. In this section, we’ll look at the API that enables you to compose object graphs with MS.DI. MS.DI requires you to register all relevant components before you can resolve them. The following listing shows one of the simplest possible uses of MS.DI.

清单 15.1 MS.DI 的最简单使用

Listing 15.1 Simplest possible use of MS.DI

var services = new ServiceCollection();

services.AddTransient<SauceBéarnaise>();

ServiceProvider container =
    services.BuildServiceProvider(validateScopes: true);

IServiceScope scope = container.CreateScope();

SauceBéarnaise sauce =
    scope.ServiceProvider.GetRequiredService<SauceBéarnaise>();

正如图 15.1已经暗示的那样,您需要一个ServiceCollection实例来配置组件。MS.DIServiceCollection相当于 Autofac 的ContainerBuilder.

As was already implied by figure 15.1, you need a ServiceCollection instance to configure components. MS.DI’s ServiceCollection is the equivalent of Autofac’s ContainerBuilder.

在这里,您使用 注册具体SauceBéarnaiseservices,这样当您要求它构建容器时,生成的容器会配置SauceBéarnaise该类。这再次使您能够SauceBéarnaise从容器中解析类。如果您不注册SauceBéarnaise组件,尝试解析它会抛出以下消息:InvalidOperationException

Here, you register the concrete SauceBéarnaise class with services, so that when you ask it to build a container, the resulting container is configured with the SauceBéarnaise class. This again enables you to resolve the SauceBéarnaise class from the container. If you don’t register the SauceBéarnaise component, the attempt to resolve it throws a InvalidOperationException with the following message:

没有注册类型“Ploeh.Samples.MenuModel.SauceBéarnaise”的服务。

No service for type 'Ploeh.Samples.MenuModel.SauceBéarnaise' has been registered.

清单 15.1所示,使用 MS.DI,您永远不会从根容器本身解析,而是从. 15.2.1 节更详细地介绍了 an是什么。IServiceScopeIServiceScope

As listing 15.1 shows, with MS.DI, you never resolve from the root container itself but from an IServiceScope. Section 15.2.1 goes into more detail about what an IServiceScope is.

作为一项安全措施,始终ServiceProvider使用带参数的BuildServiceProvider重载来构建validateScopes设置为true,如代码清单 15.1所示。这可以防止从根容器中意外解析Scoped实例。随着 ASP.NET Core 2.0 的引入,当应用程序在开发环境中运行时,由框架validateScopes自动设置为true,但最好在开发环境之外也启用验证。这意味着您必须手动呼叫。BuildServiceProvider(true)

As a safety measure, always build the ServiceProvider using the BuildServiceProvider overload with the validateScopes argument set to true, as shown in listing 15.1. This prevents the accidental resolution of Scoped instances from the root container. With the introduction of ASP.NET Core 2.0, validateScopes is automatically set to true by the framework when the application is running in the development environment, but it’s best to enable validation even outside the development environment as well. This means you’ll have to call BuildServiceProvider(true) manually.

MS.DI 不仅可以使用无参数构造函数解析具体类型,还可以使用其他Dependencies自动连接类型。所有这些依赖项都需要注册。大多数情况下,您希望针对接口进行编程,因为这会引入松散耦合。为了支持这一点,MS.DI 允许您将抽象映射到具体类型。

Not only can MS.DI resolve concrete types with parameterless constructors, it can also Auto-Wire a type with other Dependencies. All these Dependencies need to be registered. For the most part, you want to program to interfaces, because this introduces loose coupling. To support this, MS.DI lets you map Abstractions to concrete types.

将抽象映射到具体类型

Mapping Abstractions to concrete types

尽管我们的应用程序的根类型通常由它们的具体类型解析,如清单 15.1所示,松散耦合要求您将抽象映射到具体类型。基于此类映射创建实例是任何DI Container提供的核心服务,但您仍然必须定义映射。在此示例中,您映射IIngredient 具体SauceBéarnaise类的接口,它允许您成功解析IIngredient

Whereas our application’s root types will typically be resolved by their concrete types as listing 15.1 showed, loose coupling requires you to map Abstractions to concrete types. Creating instances based on such maps is the core service offered by any DI Container, but you must still define the map. In this example, you map the IIngredient interface to the concrete SauceBéarnaise class, which allows you to successfully resolve IIngredient:

var services = new ServiceCollection();

services.AddTransient<IIngredient, SauceBéarnaise>();  ①  

var container = services.BuildServiceProvider(true);

IServiceScope scope = container.CreateScope();

IIngredient sauce = scope.ServiceProvider
    .GetRequiredService<IIngredient>();    ②  

在这里,该方法允许使用Transient LifestyleAddTransient将具体类型映射到特定的抽象。由于之前的调用,现在可以解析为.AddTransientSauceBéarnaiseIIngredient

Here, the AddTransient method allows a concrete type to be mapped to a particular Abstraction using the Transient Lifestyle. Because of the previous AddTransient call, SauceBéarnaise can now be resolved as IIngredient.

在许多情况下,通用 API 就是您所需要的。不过,在某些情况下,您需要一种更弱类型的方式来解析服务。这也是可能的。

In many cases, the generic API is all you need. Still, there are situations where you’ll need a more weakly typed way to resolve services. This is also possible.

解决弱类型服务

Resolving weakly typed services

有时您不能使用通用 API,因为您在设计时不知道合适的类型。您只有一个Type实例,但您仍然希望获得该类型的实例。您在 7.3 节中看到了一个示例,我们在其中讨论了 ASP.NET Core MVC 的IControllerActivator类。相关的方法是这个:

Sometimes you can’t use a generic API because you don’t know the appropriate type at design time. All you have is a Type instance, but you’d still like to get an instance of that type. You saw an example of that in section 7.3, where we discussed ASP.NET Core MVC’s IControllerActivator class. The relevant method is this one:

object Create(ControllerContext context);

如前面清单 7.8 所示,ControllerContext捕获控制器的Type,您可以使用ControllerTypeInfo属性的ActionDescriptor属性提取它:

As shown previously in listing 7.8, the ControllerContext captures the controller’s Type, which you can extract using the ControllerTypeInfo property of the ActionDescriptor property:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();

因为你只有一个Type实例,所以你不能使用泛型,而必须求助于弱类型的 API。MS.DI 提供了该GetRequiredService方法的弱类型重载,可让您实现该Create方法:

Because you only have a Type instance, you can’t use generics, but must resort to a weakly typed API. MS.DI offers a weakly typed overload of the GetRequiredService method that lets you implement the Create method:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return scope.ServiceProvider.GetRequiredService(controllerType);

的弱类型重载允许您传递变量GetRequiredServicecontrollerType直接到 MS.DI。通常,这意味着您必须将返回值转换为某种抽象,因为弱类型GetRequiredService方法回报object。但是,在 的情况下IControllerActivator,这不是必需的,因为 ASP.NET Core MVC 不需要控制器来实现任何接口或基类。

The weakly typed overload of GetRequiredService lets you pass the controllerType variable directly to MS.DI. Typically, this means you have to cast the returned value to some Abstraction, because the weakly typed GetRequiredService method returns object. In the case of IControllerActivator, however, this isn’t required, because ASP.NET Core MVC doesn’t require controllers to implement any interface or base class.

无论GetRequiredService您使用哪种重载,MS.DI 都保证它会返回请求类型的实例,或者在存在无法满足的依赖项时抛出异常。正确配置所有必需的依赖项后,MS.DI 可以自动连接请求的类型。

No matter which overload of GetRequiredService you use, MS.DI guarantees that it’ll return an instance of the requested type or throw an exception if there are Dependencies that can’t be satisfied. When all required Dependencies have been properly configured, MS.DI can Auto-Wire the requested type.

为了能够解析请求的类型,所有松散耦合的依赖项必须已预先配置。让我们研究一下配置 MS.DI 的方法。

To be able to resolve the requested type, all loosely coupled Dependencies must have been previously configured. Let’s investigate the ways that you can configure MS.DI.

15.1.2 配置ServiceCollection

15.1.2 Configuring the ServiceCollection

正如我们在 12.2 节中讨论的那样,您可以通过几种概念上不同的方式配置DI 容器。图 12.5 查看了选项:配置文件、配置即代码自动注册图 15.2再次显示了这些选项。

As we discussed in section 12.2, you can configure a DI Container in several conceptually different ways. Figure 12.5 reviewed the options: configuration files, Configuration as Code, and Auto-Registration. Figure 15.2 shows these options again.

15-02.eps

图 15.2针对显式维度和绑定程度显示的配置DI 容器 的最常用方法

Figure 15.2 The most common ways to configure a DI Container shown against dimensions of explicitness and the degree of binding

虽然没有Auto-Registration API,但在某种程度上,您可以借助.NET 的LINQ 和反射API 实现程序集扫描。在我们讨论这个之前,我们将首先讨论 MS.DI 的配置即代码API。

Although there’s no Auto-Registration API, to some extent, you can implement assembly scanning with the help of .NET’s LINQ and reflection APIs. Before we discuss this, we’ll start with a discussion of MS.DI’s Configuration as Code API.

配置ServiceCollection使用配置作为代码

Configuring the ServiceCollection using Configuration as Code

在 15.1.1 节中,您简要了解了 MS.DI 的强类型配置 API。在这里,我们将更详细地研究它。

In section 15.1.1, you saw a brief glimpse of MS.DI’s strongly typed configuration API. Here, we’ll examine it in greater detail.

MS.DI 中的所有配置都使用ServiceCollection类公开的 API,尽管大多数方法都是扩展方法。最常用的AddTransient方法之一是您已经看到的方法:

All configuration in MS.DI uses the API exposed by the ServiceCollection class, although most of the methods are extension methods. One of the most commonly used methods is the AddTransient method that you’ve already seen:

services.AddTransient<IIngredient, SauceBéarnaise>();

注册SauceBéarnaiseIIngredient隐藏具体类,因此您无法再SauceBéarnaise通过此注册进行解析。但是您可以通过将注册替换为以下内容来解决此问题:

Registering SauceBéarnaise as IIngredient hides the concrete class so that you can no longer resolve SauceBéarnaise with this registration. But you can fix this by replacing the registration with the following:

services.AddTransient<SauceBéarnaise>();
services.AddTransient<IIngredient>(
    c => c.GetRequiredService<SauceBéarnaise>());  ①  

您无需注册IIngredient使用 的自动装配重载AddTransient,而是注册一个代码块,该代码块在被调用时将调用转发到具体的注册SauceBéarnaise

Instead of making the registration for IIngredient using the Auto-Wiring overload of AddTransient, you register a code block that, when called, forwards the call to the registration of the concrete SauceBéarnaise.

在实际应用中,你总是有不止一个抽象要映射,所以你必须配置多个映射。这是通过多次调用其中一种Add...方法来完成的:

In real applications, you always have more than one Abstraction to map, so you must configure multiple mappings. This is done with multiple calls to one of the Add... methods:

services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<ICourse, Course>();

这映射IIngredientSauceBéarnaiseICourseCourse没有类型重叠,所以应该很明显发生了什么。但是您也可以多次注册相同的抽象:

This maps IIngredient to SauceBéarnaise, and ICourse to Course. There’s no overlap of types, so it should be pretty evident what’s going on. But you can also register the same Abstraction several times:

services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<IIngredient, Steak>();

在这里,你注册IIngredient了两次。如果您解析IIngredient,您将获得 的一个实例Steak。最后一次注册获胜,但不会忘记以前的注册。MS.DI 可以为同一个抽象处理多个配置,但我们将在 15.4 节回到这个主题。

Here, you register IIngredient twice. If you resolve IIngredient, you get an instance of Steak. The last registration wins, but previous registrations aren’t forgotten. MS.DI can handle multiple configurations for the same Abstraction, but we’ll get back to this topic in section 15.4.

虽然有更高级的选项可用于配置 MS.DI,但您可以使用此处显示的方法配置整个应用程序。但是,为了避免对容器配置进行过多的显式维护,您可以考虑使用自动注册的更基于约定的方法。

Although there are more-advanced options available for configuring MS.DI, you can configure an entire application with the methods shown here. But to save yourself from too much explicit maintenance of container configuration, you could instead consider a more convention-based approach using Auto-Registration.

ServiceCollection使用自动注册进行配置

Configuring ServiceCollection using Auto-Registration

在许多情况下,注册将是相似的。这样的注册维护起来很乏味,并且显式注册每个组件可能不是最有效的方法,正如我们在 12.3.3 节中讨论的那样。

In many cases, registrations will be similar. Such registrations are tedious to maintain, and explicitly registering each and every component might not be the most productive approach, as we discussed in section 12.3.3.

考虑一个包含许多IIngredient实现的库。您可以单独配置每个类,但这会导致Type提供给Add...方法的实例列表不断变化。更糟糕的是,每次添加新IIngredient实现时,如果您希望它可用,还必须显式地向容器注册它。IIngredient声明在给定程序集中找到的所有实现都应该注册会更有成效。

Consider a library that contains many IIngredient implementations. You can configure each class individually, but it’ll result in an ever-changing list of Type instances supplied to the Add... methods. What’s worse is that every time you add a new IIngredient implementation, you must also explicitly register it with the container if you want it to be available. It would be more productive to state that all implementations of IIngredient found in a given assembly should be registered.

如前所述,MS.DI 不包含自动注册API。这意味着你必须自己做。这在某种程度上是可能的,在本节中,我们将通过一个简单的示例来展示如何实现,但将对可能性和限制的更详细讨论推迟到第 15.4 节。让我们看一下如何注册一系列IIngredient注册:

As stated previously, MS.DI contains no Auto-Registration API. This means you have to do it yourself. This is possible to some degree, and in this section, we’ll show how with a simple example but delay more detailed discussions of the possibilities and limitations until section 15.4. Let’s take a look how you can register a sequence of IIngredient registrations:

Assembly ingredientsAssembly = typeof(Steak).Assembly;

var ingredientTypes =
    from type in ingredientsAssembly.GetTypes()    ①  
    where !type.IsAbstract    ①  
    where typeof(IIngredient).IsAssignableFrom(type)    ①  
    select type;    ①  

foreach (var type in ingredientTypes)
{
    services.AddTransient(typeof(IIngredient), type);  ②  
}

前面的示例无条件地配置IIngredient接口的所有实现,但您可以提供使您能够仅选择一个子集的过滤器。这是一个基于约定的扫描,您只添加名称以Sauce开头的类:

The previous example unconditionally configures all implementations of the IIngredient interface, but you can provide filters that enable you to select only a subset. Here’s a convention-based scan where you add only classes whose name starts with Sauce:

Assembly ingredientsAssembly = typeof(Steak).Assembly;

var ingredientTypes =
    from type in ingredientsAssembly.GetTypes()
    where !type.IsAbstract
    where typeof(IIngredient).IsAssignableFrom(type)
    where type.Name.StartsWith("Sauce")    ①  
    select type;

foreach (var type in ingredientTypes)
{
    services.AddTransient(typeof(IIngredient), type);
}

除了从程序集中选择正确的类型外,自动注册的另一部分正在定义正确的映射。在前面的示例中,您使用AddTransient具有特定接口的方法来针对该接口注册所有选定的类型。

Apart from selecting the correct types from an assembly, another part of Auto-Registration is defining the correct mapping. In the previous examples, you used the AddTransient method with a specific interface to register all selected types against that interface.

但有时您会想要使用不同的约定。假设您使用抽象基类而不是接口,并且您希望在名称以Policy结尾的程序集中按其基类型注册所有类型:

But sometimes you’ll want to use different conventions. Let’s say that instead of interfaces, you use abstract base classes, and you want to register all types in an assembly where the name ends with Policy by their base type:

Assembly policiesAssembly = typeof(DiscountPolicy).Assembly;

var policyTypes =
    from type in policiesAssembly.GetTypes()    ①  
    where type.Name.EndsWith("Policy")    ②  
    select type;

foreach (var type in policyTypes)
{
    services.AddTransient(type.BaseType, type);    ③  
}

尽管 MS.DI 不包含基于约定的 API,但通过使用现有的 .NET 框架 API,基于约定的注册是可能的。当谈到泛型时,这就变成了另一场球赛,我们将在接下来讨论。

Even though MS.DI contains no convention-based API, by making use of existing .NET framework APIs, convention-based registrations are possible. This becomes a different ball game when it comes to generics, as we’ll discuss next.

通用抽象的自动注册

Auto-Registration of generic Abstractions

在第 10 章的课程中,您重构了令人讨厌的大IProductService界面ICommandService<TCommand>界面清单 10.12。这又是那个抽象

During the course of chapter 10, you refactored the big, obnoxious IProductService interface to the ICommandService<TCommand> interface of listing 10.12. Here’s that Abstraction again:

public interface ICommandService<TCommand>
{
    void Execute(TCommand command);
}

如第 10 章所述,每个命令参数对象代表一个用例,每个用例将有一个实现。以清单 10.8 为例。它实施了“调整库存”用例。下面的清单再次显示了这个类。AdjustInventoryService

As discussed in chapter 10, every command Parameter Object represents a use case, and there’ll be a single implementation per use case. The AdjustInventoryService of listing 10.8 was given as an example. It implemented the “adjust inventory” use case. The following listing shows this class again.

清单 15.2AdjustInventoryService来自第 10 章 的

Listing 15.2 The AdjustInventoryService from chapter 10

public class AdjustInventoryService : ICommandService<AdjustInventory>
{
    private readonly IInventoryRepository repository;

    public AdjustInventoryService(IInventoryRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(AdjustInventory command)
    {
        var productId = command.ProductId;

        ...
    }
}

任何相当复杂的系统都可以轻松实现数百个用例,这是使用自动注册的理想选择。但是由于 MS.DI 缺乏自动注册支持,您必须编写大量代码才能使其运行。下一个清单提供了一个例子。

Any reasonably complex system will easily implement hundreds of use cases, and this is an ideal candidate for using Auto-Registration. But because of the lack of Auto-Registration support by MS.DI, you’ll have to write a fair amount of code to get this running. The next listing provides an example of this.

清单 15.3 实现的自动注册ICommandService<TCommand>

Listing 15.3 Auto-Registration of ICommandService<TCommand> implementations

Assembly assembly = typeof(AdjustInventoryService).Assembly;

var mappings =
    from type in assembly.GetTypes()
    where !type.IsAbstract    ①  
    where !type.IsGenericType    ②  
    from i in type.GetInterfaces()    ③  
    where i.IsGenericType    ③  
    where i.GetGenericTypeDefinition()    ③  
        == typeof(ICommandService<>)    ③  
    select new { service = i, type };    ③  

foreach (var mapping in mappings)
{
    services.AddTransient(    ④  
        mapping.service,    ④  
        mapping.type);    ④  
}

与前面的清单一样,您可以充分利用 .NET 的 LINQ 和反射 API 来允许从提供的程序集中选择类。使用提供的开放式通用接口,您可以遍历程序集类型列表,并注册实现封闭式通用版本的所有类型ICommandService<TCommand>。例如,这意味着它AdjustInventoryService已注册,因为它实现ICommandService<AdjustInventory>了 ,它是 . 的封闭通用版本ICommandService<TCommand>

As in the previous listings, you make full use of .NET’s LINQ and Reflection APIs to allow selecting classes from the supplied assembly. Using the supplied open-generic interface, you iterate through the list of assembly types, and register all types that implement a closed-generic version of ICommandService<TCommand>. What this means, for instance, is that AdjustInventoryService is registered because it implements ICommandService<AdjustInventory>, which is a closed-generic version of ICommandService<TCommand>.

本节介绍了 MS.DI DI 容器并演示了这些基本机制:如何配置ServiceCollection,以及随后如何使用构造ServiceProvider的来解析服务。只需调用一次GetRequiredService方法即可解析服务,因此复杂性涉及配置容器。API 主要支持Configuration as Code,尽管在某种程度上可以在其之上构建自动注册。但是,正如您稍后将看到的那样,不支持自动注册将导致代码非常复杂且难以维护。到目前为止,我们只了解了最基本的 API,但我们还没有涉及另一个领域——如何管理组件生命周期。

This section introduced the MS.DI DI Container and demonstrated these fundamental mechanics: how to configure a ServiceCollection, and, subsequently, how to use the constructed ServiceProvider to resolve services. Resolving services is done with a single call to the GetRequiredService method, so the complexity involves configuring the container. The API primarily supports Configuration as Code, although to some extend Auto-Registration can be built on top of it. As you’ll see later, however, the lack of support for Auto-Registration will lead to quite complex and hard-to-maintain code. Until now, we’ve only looked at the most basic API, but there’s another area we have yet to cover — how to manage component lifetime.

15.2 管理生命周期

15.2 Managing lifetime

在第 8 章中,我们讨论了生命周期管理,包括最常见的概念生命周期样式,例如TransientSingletonScoped。MS.DI 支持这三种Lifestyles,并允许您配置所有服务的生命周期。表 15.2中显示的生活方式作为 API 的一部分提供。

In chapter 8, we discussed Lifetime Management, including the most common conceptual lifetime styles such as Transient, Singleton, and Scoped. MS.DI supports these three Lifestyles and lets you configure the lifetime of all services. The Lifestyles shown in table 15.2 are available as part of the API.

表 15.2 Microsoft.Extensions.DependencyInjection生活方式
微软名称花样名称注释
短暂的短暂的实例由容器跟踪并处理。
单例单例实例在容器被处置时被处置。
范围范围实例在同一个IServiceScope. 在范围的生命周期内跟踪实例,并在范围被处置时被处置。

MS.DI 的TransientSingleton实现相当于第 8 章中描述的一般Lifestyles,因此本章我们不会花太多时间在它们上面。相反,在本节中,您将看到如何在代码中为组件定义Lifestyles 。到本节结束时,您应该能够在您自己的应用程序中使用 MS.DI 的Lifestyles 。让我们首先回顾一下如何为组件配置实例范围。

MS.DI’s implementation of Transient and Singleton are equivalent to the general Lifestyles described in chapter 8, so we won’t spend much time on them in this chapter. Instead, in this section, you’ll see how you can define Lifestyles for components in code. By the end of this section, you should be able to use MS.DI’s Lifestyles in your own application. Let’s start by reviewing how to configure instance scopes for components.

15.2.1 配置生活方式

15.2.1 Configuring Lifestyles

在本节中,我们将回顾如何使用 MS.DI管理生活方式。生活方式被配置为注册组件的一部分。就这么简单:

In this section, we’ll review how to manage Lifestyles with MS.DI. A Lifestyle is configured as part of registering components. It’s as easy as this:

services.AddSingleton<SauceBéarnaise>();

这将具体SauceBéarnaise类配置为单例,以便每次SauceBéarnaise请求时返回相同的实例。如果要将抽象映射到具有特定Lifestyle的具体类,可以使用AddSingleton带有两个通用参数的重载:

This configures the concrete SauceBéarnaise class as a Singleton so that the same instance is returned each time SauceBéarnaise is requested. If you want to map an Abstraction to a concrete class with a specific Lifestyle, you can use the AddSingleton overload with two generic arguments:

services.AddSingleton<IIngredient, SauceBéarnaise>();

与其他DI Container相比,MS.DI 在为组件配置Lifestyles时没有太多选择。它是以一种相当声明的方式完成的。虽然配置通常很容易,但您一定不要忘记,某些Lifestyles涉及长期存在的对象,只要它们存在就使用资源。

Compared to other DI Containers, there aren’t many options in MS.DI when it comes to configuring Lifestyles for components. It’s done in a rather declarative fashion. Although configuration is typically easy, you mustn’t forget that some Lifestyles involve long-lived objects that use resources as long as they’re around.

15.2.2 释放组件

15.2.2 Releasing components

正如 8.2.2 节中所讨论的,当你用完它们时释放对象是很重要的。与 Autofac 和 Simple Injector 类似,MS.DI 没有明确Release的方法,而是使用一个名为scopes的概念。范围可以被视为特定于请求的缓存。如图15.3所示,它定义了组件可以重用的边界。

As discussed in section 8.2.2, it’s important to release objects when you’re done with them. Similar to Autofac and Simple Injector, MS.DI has no explicit Release method, but instead uses a concept called scopes. A scope can be regarded as a request-specific cache. As figure 15.3 illustrates, it defines a boundary where components can be reused.

AnIServiceScope定义了一个缓存,您可以将其用于特定的持续时间或目的;最明显的例子是网络请求。当从 请求Scoped组件时,您总是收到相同的实例。与真正的单例的不同之处在于,如果您查询第二个范围,您将获得另一个实例。IServiceScope

An IServiceScope defines a cache that you can use for a particular duration or purpose; the most obvious example is a web request. When a Scoped component is requested from an IServiceScope, you always receive the same instance. The difference from true Singletons is that if you query a second scope, you’ll get another instance.

15-03.eps

图 15.3 Microsoft.Extensions.DependencyInjection 的范围充当可以在有限的持续时间或目的内共享组件的容器。

Figure 15.3 Microsoft.Extensions.DependencyInjection’s scopes act as containers that can share components for a limited duration or purpose.

作用域的一个重要特性是它们允许您在作用域完成时正确地释放组件。CreateScope您使用特定实现的方法创建一个新范围,并通过调用其方法IServiceProvider释放所有适当的组件:Dispose

One of the important features of scopes is that they let you properly release components when the scope completes. You create a new scope with the CreateScope method of a particular IServiceProvider implementation, and release all appropriate components by invoking its Dispose method:

using (IServiceScope scope = container.CreateScope())  ①  
{
    IMeal meal = scope.ServiceProvider
        .GetRequiredService<IMeal>();    ②  

    meal.Consume();    ③  

}    ④  

CreateScope通过调用方法从容器中创建一个新范围. 返回值 implements IDisposable,因此您可以将其包装在一个using块中。因为IServiceScope包含一个ServiceProvider属性实现与容器本身实现的相同接口,您可以使用作用域以与容器本身完全相同的方式解析组件。

A new scope is created from the container by invoking the CreateScope method. The return value implements IDisposable, so you can wrap it in a using block. Because IServiceScope contains a ServiceProvider property that implements the same interface that the container itself implements, you can use the scope to resolve components in exactly the same way as with the container itself.

当你用完示波器后,你可以处理掉它。对于一个using块,当您退出该块时,这会自动发生,但您也可以选择通过调用该Dispose方法来显式处理它。当你处理示波器时,你还释放范围创建的所有组件;在这里,意味着你释放餐点对象图。

When you’re done with the scope, you can dispose of it. With a using block, this happens automatically when you exit that block, but you can also choose to explicitly dispose of it by invoking the Dispose method. When you dispose of the scope, you also release all the components that were created by the scope; here, it means that you release the meal object graph.

请注意,组件的依赖关系始终在组件范围内或组件范围内解析。例如,如果您需要将Transient Dependency注入到Singleton中,那么该Transient Dependency将来自根容器,即使您是从嵌套范围解析Singleton也是如此。这会跟踪根容器内的Transient,并防止在处理范围时将其处理掉。否则,Singleton消费者会崩溃,因为它在根容器中保持活动状态,同时依赖于已处理的组件。

Note that Dependencies of a component are always resolved at or below the component’s scope. For example, if you need a Transient Dependency injected into a Singleton, that Transient Dependency will come from the root container, even if you’re resolving the Singleton from a nested scope. This tracks the Transient within the root container and prevents it from being disposed of when the scope gets disposed of. The Singleton consumer would otherwise break, because it’s kept alive in the root container while depending on a component that was disposed of.

在本节的前面,您看到了如何将组件配置为SingletonsTransients。将组件配置为具有Scoped Lifestyle的方法类似:

Earlier in this section, you saw how to configure components as Singletons or Transients. Configuring a component to have a Scoped Lifestyle is done in a similar way:

services.AddScoped<IIngredient, SauceBéarnaise>();

和方法类似,可以使用方法AddTransientAddSingletonAddScoped声明组件的生命周期应该遵循创建实例的范围。

Similar to the AddTransient and AddSingleton methods, you can use the AddScoped method to state that the component’s lifetime should follow the scope that created the instance.

由于它们的性质,单例在容器本身的生命周期内永远不会被释放。尽管如此,如果您不再需要容器,您甚至可以释放这些组件。这是通过处理容器本身来完成的:

Due to their nature, Singletons are never released for the lifetime of the container itself. Still, you can release even those components if you don’t need the container any longer. This is done by disposing of the container itself:

container.Dispose();

实际上,这并不像处理范围那么重要,因为容器的生命周期往往与其支持的应用程序的生命周期密切相关。只要应用程序运行,您通常会保留容器,因此您只会在应用程序关闭时处理它。在这种情况下,内存将被操作系统回收。

In practice, this isn’t nearly as important as disposing of a scope, because the lifetime of a container tends to correlate closely with the lifetime of the application it supports. You normally keep the container around as long as the application runs, so you’d only dispose of it when the application shuts down. In this case, memory would be reclaimed by the operating system.

我们的MS.DI生命周期管理之旅到此结束。可以使用混合的Lifestyles配置组件,即使您注册了同一个Abstraction的多个实现也是如此。到目前为止,您已经通过隐式假设所有组件都使用构造函数注入来允许容器连接依赖项。但情况并非总是如此。在下一节中,我们将回顾如何处理必须以特殊方式实例化的类。

This completes our tour of Lifetime Management with MS.DI. Components can be configured with mixed Lifestyles, and this is true even when you register multiple implementations of the same Abstraction. Until now, you’ve allowed the container to wire Dependencies by implicitly assuming that all components use Constructor Injection. But this isn’t always the case. In the next section, we’ll review how to deal with classes that must be instantiated in special ways.

15.3 注册困难的 API

15.3 Registering difficult APIs

到目前为止,我们已经考虑了如何配置使用构造函数注入的组件。构造函数注入的众多好处之一是DI 容器(例如 MS.DI)可以轻松理解如何在依赖图中组合和创建所有类。当 API 表现不佳时,这一点就不太清楚了。

Until now, we’ve considered how you can configure components that use Constructor Injection. One of the many benefits of Constructor Injection is that DI Containers such as MS.DI can easily understand how to compose and create all classes in a Dependency graph. This becomes less clear when APIs are less well behaved.

在本节中,您将看到如何处理原始构造函数参数和静态工厂。这些都需要你特别注意。让我们首先看一下采用基本类型(例如字符串或整数)作为构造函数参数的类。

In this section, you’ll see how to deal with primitive constructor arguments and static factories. These all require your special attention. Let’s start by looking at classes that take primitive types, such as strings or integers, as constructor arguments.

15.3.1 配置原始依赖

15.3.1 Configuring primitive Dependencies

只要您将抽象注入消费者,一切都很好。但是,当构造函数依赖于基本类型(例如字符串、数字或枚举)时,这就变得更加困难。对于将连接字符串作为构造函数参数的数据访问实现尤其如此,但这是适用于所有字符串和数字的更普遍的问题。

As long as you inject Abstractions into consumers, all is well. But it becomes more difficult when a constructor depends on a primitive type, such as a string, a number, or an enum. This is particularly the case for data access implementations that take a connection string as constructor parameter, but it’s a more general issue that applies to all strings and numbers.

从概念上讲,将字符串或数字注册为容器中的组件并不总是有意义的。使用通用类型约束,MS.DI 甚至阻止从其通用 API 注册值类型,如数字和枚举。另一方面,对于非通用 API,这仍然是可能的。以这个构造函数为例:

Conceptually, it doesn’t always make sense to register a string or number as a component in a container. Using generic type constraints, MS.DI even blocks the registration of value types like numbers and enums from its generic API. With the non-generic API, on the other hand, this is still possible. Consider as an example this constructor:

public ChiliConCarne(Spiciness spiciness)

在这个例子中,Spiciness是一个枚举:

In this example, Spiciness is an enum:

public enum Spiciness { Mild, Medium, Hot }

如果您希望的所有消费者都Spiciness使用相同的值,您可以注册Spiciness并且ChiliConCarne彼此独立:

If you want all consumers of Spiciness to use the same value, you can register Spiciness and ChiliConCarne independently of each other:

services.AddSingleton(    ①  
    typeof(Spiciness), Spiciness.Medium);    ①  

services.AddTransient<ICourse, ChiliConCarne>();  ②  

当您随后 resolveChiliConCarne时,它​​将有一个Medium Spiciness,所有其他依赖于的组件也将有一个SpicinessChiliConCarne如果你更愿意在更精细的层面上控制和之间的关系Spiciness,你可以使用代码块,我们稍后将在 15.3.3 节中谈到这一点。

When you subsequently resolve ChiliConCarne, it’ll have a Medium Spiciness, as will all other components with a Dependency on Spiciness. If you’d rather control the relationship between ChiliConCarne and Spiciness on a finer level, you can use a code block, which is something we get back to in a moment in section 15.3.3.

此处描述的选项使用自动装配为组件提供具体值。然而,更方便的解决方案是将原始依赖项提取到参数对象中。

The option described here uses Auto-Wiring to provide a concrete value to a component. A more convenient solution, however, is to extract the primitive Dependencies into Parameter Objects.

15.3.2 提取对参数对象的原始依赖

15.3.2 Extracting primitive Dependencies to Parameter Objects

在第 10.3.3 节中,我们讨论了参数对象的引入如何减轻导致的开闭原则违规IProductService。然而,参数对象也是减少歧义的好工具。例如,Spiciness可以用更笼统的术语将课程描述为调味品。调味可能包括其他属性,例如咸味,因此您可以将Spiciness咸味包装在一个Flavoring类中:

In section 10.3.3, we discussed how the introduction of Parameter Objects allowed mitigating the Open/Closed Principle violation that IProductService caused. Parameter Objects, however, are also a great tool to mitigate ambiguity. For example, the Spiciness of a course could be described in more general terms as a flavoring. Flavoring might include other properties, such as saltiness, so you can wrap Spiciness and the saltiness in a Flavoring class:

public class Flavoring
{
    public readonly Spiciness Spiciness;
    public readonly bool ExtraSalty;

    public Flavoring(Spiciness spiciness, bool extraSalty)
    {
        this.Spiciness = spiciness;
        this.ExtraSalty = extraSalty;
    }
}

正如我们在 10.3.3 节中提到的,Parameter Objects 有一个参数是完全没问题的。目标是消除歧义,而不仅仅是在技术层面上。这样的参数对象的名称可能会更好地描述您的代码在功能级别上所做的事情,就像Flavoring该类所做的那样优雅。随着Flavoring参数对象的引入,现在可以在不引入歧义的情况下自动连接任何ICourse需要一些调味的实现:

As we mentioned in section 10.3.3, it’s perfectly fine for Parameter Objects to have one parameter. The goal is to remove ambiguity, and not just on the technical level. Such a Parameter Object’s name might do a better job describing what your code does on a functional level, as the Flavoring class so elegantly does. With the introduction of the Flavoring Parameter Object, it now becomes possible to Auto-Wire any ICourse implementation that requires some flavoring without introducing ambiguity:

var flavoring = new Flavoring(Spiciness.Medium, extraSalty: true);
services.AddSingleton<Flavoring>(flavoring);

container.AddTransient<ICourse, ChiliConCarne>();

此代码创建该类的单个实例FlavoringFlavoring成为课程的配置对象。因为只有一个实例,所以您可以使用接受预创建实例的重载Flavoring在 MS.DI 中注册它。AddSingleton<T>

This code creates a single instance of the Flavoring class. Flavoring becomes a configuration object for courses. Because there’ll only be one Flavoring instance, you can register it in MS.DI using the AddSingleton<T> overload that accepts a precreated instance.

将原始依赖项提取到参数对象中应该是您优于前面讨论的选项的首选,因为参数对象在功能和技术级别上都消除了歧义。但是,它确实需要更改组件的构造函数,这可能并不总是可行的。在这种情况下,注册代理人是您的第二好选择。

Extracting primitive Dependencies into Parameter Objects should be your preference over the previously discussed option, because Parameter Objects remove ambiguity, at both the functional and technical levels. It does, however, require a change to a component’s constructor, which might not always be feasible. In this case, registering a delegate is your second-best pick.

15.3.3 使用代码块注册对象

15.3.3 Registering objects with code blocks

使用原始值创建组件的另一种选择是使用其中一种Add...方法,它允许您提供创建组件的委托:

Another option for creating a component with a primitive value is to use one of the Add... methods, which let you supply a delegate that creates the component:

services.AddTransient<ICourse>(c => new ChiliConCarne(Spiciness.Hot));

你已经看到了这个AddTransient方法之前我们在 15.1.2 节中讨论 torn Lifestyles时会超载。每次解析服务时都会ChiliConCarne调用构造函数。以下示例显示了此扩展方法的定义:SpicinessICourseAddTransient<TService>

You already saw this AddTransient method overload previously, when we discussed torn Lifestyles in section 15.1.2. The ChiliConCarne constructor is invoked with a hot Spiciness every time the ICourse service is resolved. The following example shows the definition of this AddTransient<TService> extension method:

public static IServiceCollection AddTransient<TService>(
    this IServiceCollection services,
    Func<IServiceProvider, TService> implementationFactory)
    where TService : class;

如您所见,此AddTransient方法接受 type 的参数Func<IServiceProvider, TService>。对于之前的注册,当 anICourse被解析时,MS.DI 将调用提供的委托并为其提供IServiceProvider属于当前IServiceScope. 有了它,您的代码块就可以解析源自同一个. 我们将在下一节中对此进行演示。IServiceScope

As you can see, this AddTransient method accepts a parameter of type Func<IServiceProvider, TService>. With respect to the previous registration, when an ICourse is resolved, MS.DI will call the supplied delegate and supply it with the IServiceProvider belonging to the current IServiceScope. With it, your code block can resolve instances that originate from the same IServiceScope. We’ll demonstrate this in the next section.

谈到ChiliConCarne类时,您可以选择自动装配或使用代码块。但其他类的限制更为严格:它们不能通过公共构造函数实例化。相反,您必须使用某种工厂来创建该类型的实例。这对于DI 容器来说总是很麻烦,因为默认情况下,它们会照看公共构造函数。考虑公共JunkFood类的这个示例构造函数:

When it comes to the ChiliConCarne class, you have a choice between Auto-Wiring or using a code block. But other classes are more restrictive: they can’t be instantiated through a public constructor. Instead, you must use some sort of factory to create instances of the type. This is always troublesome for DI Containers, because, by default, they look after public constructors. Consider this example constructor for the public JunkFood class:

internal JunkFood(string name)

尽管JunkFood该类可能是公共的,但构造函数是内部的。在此示例中,JunkFood应该通过静态JunkFoodFactory类创建实例:

Even though the JunkFood class might be public, the constructor is internal. In this example, instances of JunkFood should instead be created through the static JunkFoodFactory class:

public static class JunkFoodFactory
{
    public static JunkFood Create(string name)
    {
        return new JunkFood(name);
    }
}

从 MS.DI 的角度来看,这是一个有问题的 API,因为围绕静态工厂没有明确且完善的约定。它需要帮助——您可以通过提供它可以执行以创建实例的代码块来提供帮助:

From MS.DI’s perspective, this is a problematic API, because there are no unambiguous and well-established conventions around static factories. It needs help — and you can give that help by providing a code block it can execute to create the instance:

services.AddTransient<IMeal>(c => JunkFoodFactory.Create("chicken meal"));

这一次,您使用该AddTransient方法通过在代码块中调用静态工厂来创建组件。JunkFoodFactory.Create每次IMeal解析都会调用,并返回结果。

This time, you use the AddTransient method to create the component by invoking a static factory within the code block. JunkFoodFactory.Create will be invoked every time IMeal is resolved, and the result will be returned.

如果您必须编写代码来创建实例,那么这比直接调用代码有什么好处呢?AddTransient通过在方法调用中使用代码块,您仍然可以获得一些东西:

If you have to write the code to create the instance, how is this in any way better than invoking the code directly? By using a code block inside a AddTransient method call, you still gain something:

  • 你映射从IMealJunkFood。这允许消费类保持松散耦合。
  • You map from IMeal to JunkFood. This allows consuming classes to stay loosely coupled.
  • 生活方式仍然可以配置。虽然会调用代码块来创建实例,但可能不会在每次请求实例时都调用它。它是默认设置,但如果将其更改为Singleton,代码块将只被调用一次,结果会被缓存并在之后重用。
  • Lifestyles can still be configured. Although the code block will be invoked to create the instance, it may not be invoked every time the instance is requested. It is by default, but if you change it to a Singleton, the code block will only be invoked once, and the result cached and reused thereafter.

在本节中,您了解了如何使用 MS.DI 来处理更难创建的 API。到目前为止,代码示例都相当简单。当您开始使用多个组件时,这会很快改变,所以现在让我们将注意力转向那个方向。

In this section, you’ve seen how you can use MS.DI to deal with more-difficult creational APIs. Up until this point, the code examples have been fairly straightforward. This will quickly change when you start to work with multiple components, so let’s now turn our attention in that direction.

15.4 使用多个组件

15.4 Working with multiple components

正如在 12.1.2 节中提到的,DI 容器在独特性上茁壮成长,但在模棱两可的情况下却很难。使用Constructor Injection时,单个构造函数优于重载构造函数,因为在别无选择时使用哪个构造函数是显而易见的。从抽象映射到具体类型时也是如此。如果您试图将多个具体类型映射到同一个抽象,就会引入歧义。

As alluded to in section 12.1.2, DI Containers thrive on distinctness but have a hard time with ambiguity. When using Constructor Injection, a single constructor is preferred over overloaded constructors, because it’s evident which constructor to use when there’s no choice. This is also the case when mapping from Abstractions to concrete types. If you attempt to map multiple concrete types to the same Abstraction, you introduce ambiguity.

尽管模棱两可的性质不受欢迎,但您经常需要处理单个抽象的多个实现。在这些情况下可能会出现这种情况:

Despite the undesirable qualities of ambiguity, you often need to work with multiple implementations of a single Abstraction. This can be the case in these situations:

  • 不同的混凝土类型用于不同的消费者。
  • Different concrete types are used for different consumers.
  • 依赖关系是序列。
  • Dependencies are sequences.
  • 正在使用装饰器或复合材料。
  • Decorators or Composites are in use.

在本节中,我们将查看这些案例中的每一个,并了解如何使用 MS.DI 解决每个问题。当我们完成后,您应该清楚地知道您可以使用 MS.DI 做什么,以及当同一抽象的多个实现在起作用时边界在哪里。让我们首先看看如何提供比Auto-Wiring提供的更细粒度的控制。

In this section, we’ll look at each of these cases and see how you can address each with MS.DI. When we’re done, you should have a good feel for what you can do with MS.DI and where the boundaries lie when multiple implementations of the same Abstraction are in play. Let’s first see how you can provide more fine-grained control than Auto-Wiring provides.

15.4.1 在多个候选人中选择

15.4.1 Selecting among multiple candidates

Auto-Wiring方便且功能强大,但提供的控制很少。只要所有抽象明确映射到具体类型,就没有问题。但是,一旦您引入了同一接口的更多实现,歧义就会浮出水面。让我们首先回顾一下 MS.DI 如何处理同一个抽象的多个注册。

Auto-Wiring is convenient and powerful but provides little control. As long as all Abstractions are distinctly mapped to concrete types, you have no problems. But as soon as you introduce more implementations of the same interface, ambiguity rears its ugly head. Let’s first recap how MS.DI deals with multiple registrations of the same Abstraction.

配置同一服务的多个实现

Configuring multiple implementations of the same service

正如您在 15.1.2 节中看到的,您可以注册同一接口的多个实现:

As you saw in section 15.1.2, you can register multiple implementations of the same interface:

services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<IIngredient, Steak>();

此示例将SteakSauceBéarnaise类注册为IIngredient服务。最后一次注册获胜,因此如果您使用 解决IIngredientGetRequired—Service<IIngredient>()您将获得一个Steak实例。

This example registers both the Steak and SauceBéarnaise classes as the IIngredient service. The last registration wins, so if you resolve IIngredient with GetRequired—Service<IIngredient>(), you’ll get a Steak instance.

您也可以要求容器解析所有IIngredient组件。MS.DI 有一个专门的方法来做到这一点,称为GetServices. 这是一个例子:

You can also ask the container to resolve all IIngredient components. MS.DI has a dedicated method to do that, called GetServices. Here’s an example:

IEnumerable<IIngredient> ingredients =
    scope.ServiceProvider.GetServices<IIngredient>();  ①  

在幕后,GetServices委托给,同时请求. 您还可以要求容器使用以下方法解析所有组件:GetRequiredServiceIEnumerable<IIngredient>>IIngredientGetRequiredService

Under the hood, GetServices delegates to GetRequiredService, while requesting an IEnumerable<IIngredient>>. You can also ask the container to resolve all IIngredient components using GetRequiredService instead:

IEnumerable<IIngredient> ingredients = scope.ServiceProvider
    .GetRequiredService<IEnumerable<IIngredient>>();

请注意,您使用的是普通GetRequiredService方法,但您请求IEnumerable<IIngredient>. 容器将此解释为约定,并为您提供IIngredient它拥有的所有组件。

Notice that you use the normal GetRequiredService method, but that you request IEnumerable<IIngredient>. The container interprets this as a convention and gives you all the IIngredient components it has.

当某个抽象有多个实现时,通常会有一个消费者依赖于一个序列。然而,有时组件需要与固定集合或同一抽象的依赖项的子集一起工作,这就是我们接下来要讨论的内容。

When there are multiple implementations of a certain Abstraction, there’ll often be a consumer that depends on a sequence. Sometimes, however, components need to work with a fixed set or a subset of Dependencies of the same Abstraction, which is what we’ll discuss next.

使用代码块消除歧义

Removing ambiguity using code blocks

与自动装配一样有用,有时您需要覆盖正常行为以提供对哪些依赖项去往何处的细粒度控制,但也可能是您需要解决不明确的 API。例如,考虑这个构造函数:

As useful as Auto-Wiring is, sometimes you need to override the normal behavior to provide fine-grained control over which Dependencies go where, but it may also be that you need to address an ambiguous API. As an example, consider this constructor:

public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)

在这种情况下,您有三个相同类型的Dependencies,每个代表一个不同的概念。在大多数情况下,您希望将每个依赖项映射到一个单独的类型。

In this case, you have three identically typed Dependencies, each of which represents a different concept. In most cases, you want to map each of the Dependencies to a separate type.

如前所述,与 Autofac 和 Simple Injector 相比,MS.DI 的功能有限。Autofac 提供键控注册,而 Simple Injector 提供条件注册来处理这种歧义,而 MS.DI 在这方面存在不足。没有任何内置功能可以执行此操作。要将这样一个模糊的 API 与 MS.DI 连接起来,您必须恢复使用代码块。

As stated previously, when compared to both Autofac and Simple Injector, MS.DI is limited in functionality. Where Autofac provides keyed registrations, and Simple Injector provides conditional registrations to deal with this kind of ambiguity, MS.DI falls short in this respect. There isn’t any built-in functionality to do this. To wire up such an ambiguous API with MS.DI, you have to revert to using a code block.

清单 15.4ThreeCourseMeal通过解析代码块中的课程进行 布线

Listing 15.4 Wiring ThreeCourseMeal by resolving courses in a code block

services.AddTransient<IMeal>(c => new ThreeCourseMeal(  ①  
    entrée: c.GetRequiredService<Rillettes>(),    ②  
    mainCourse: c.GetRequiredService<CordonBleu>(),    ②  
    dessert: c.GetRequiredService<CrèmeBrûlée>()));    ②  

此注册从Auto-Wiring恢复并ThreeCourseMeal改为使用委托构造。幸运的是,这三个ICourse实现本身仍然是Auto-Wired。要为 恢复自动接线ThreeCourseMeal,您可以使用 MS.DI 的ActivatorUtilities类。

This registration reverts from Auto-Wiring and constructs the ThreeCourseMeal using a delegate instead. Fortunately, the three ICourse implementations themselves are still Auto-Wired. To bring Auto-Wiring back for the ThreeCourseMeal, you make use of MS.DI’s ActivatorUtilities class.

使用消除歧义ActivatorUtilities

Removing ambiguity using ActivatorUtilities

在这个例子中,缺少自动装配ThreeCourseMeal并不是什么问题,因为在这种情况下,您覆盖了所有构造函数参数。ThreeCourseMeal如果包含更多依赖项,这可能会有所不同:

The lack of Auto-Wiring of ThreeCourseMeal isn’t that problematic in this example because, in this case, you override all constructor arguments. This could be different if ThreeCourseMeal contained more Dependencies:

public ThreeCourseMeal(
    ICourse entrée,
    ICourse mainCourse,
    ICourse dessert,
    ...    ①  
    )

MS.DI 包含一个名为的实用程序类ActivatorUtilities,它允许自动装配一个类的Dependencies ,同时通过显式提供它们的值来覆盖其他Dependencies 。使用ActivatorUtilities,您可以重写以前的注册。

MS.DI contains a utility class called ActivatorUtilities that allows Auto-Wiring a class’s Dependencies, while overriding other Dependencies by explicitly supplying their values. Using ActivatorUtilities, you can rewrite the previous registration.

清单 15.5 接线ThreeCourseMeal使用ActivatorUtilities

Listing 15.5 Wiring ThreeCourseMeal using ActivatorUtilities

services.AddTransient<IMeal>(c =>
    ActivatorUtilities.CreateInstance<ThreeCourseMeal>(  ①  
        c,    ②  
        new object[]    ③  
        {    ③  
            c.GetRequiredService<Rillettes>(),    ③  
            c.GetRequiredService<CordonBleu>(),    ③  
            c.GetRequiredService<MousseAuChocolat>()    ③  
        }));

这个例子使用了ActivatorUtilitiesCreateInstance<T>方法,定义如下:

This example makes use of the ActivatorUtilities’s CreateInstance<T> method, defined as follows:

public static T CreateInstance<T>(
    IServiceProvider provider,
    params object[] parameters);

CreateInstance<T>方法_创建所提供的新实例T。它遍历提供的parameters数组并将每个参数与兼容的构造函数参数匹配。然后它使用提供的 解析剩余的、不匹配的构造函数参数IServiceProvider

The CreateInstance<T> method creates a new instance of the supplied T. It goes through the supplied parameters array and matches each parameter to a compatible constructor parameter. Then it resolves the remaining, unmatched constructor parameters with the supplied IServiceProvider.

因为所有三个已解决的课程都实现ICourse了,所以调用中仍然存在歧义。通过从左到右CreateInstance<T>应用提供的内容来解决这种歧义。parameters这意味着因为Rillettesparameters数组中的第一个元素,所以它将应用于ThreeCourseMeal构造函数的第一个兼容参数。这是entrée类型的参数ICourse

Because all three resolved courses implement ICourse, there’s still ambiguity in the call. CreateInstance<T> resolves this ambiguity by applying the supplied parameters from left to right. This means that because Rillettes is the first element in the parameters array, it’ll be applied to the first compatible parameter of the ThreeCourseMeal constructor. This is the entrée parameter of type ICourse.

清单 15.4相比,清单 15.5有很大的缺点。清单 15.4已通过编译器验证。对构造函数的任何重构要么允许该代码继续工作,要么因编译错误而失败。

When compared to listing 15.4, there’s a big downside to listing 15.5. Listing 15.4 is verified by the compiler. Any refactoring to the constructor would either allow that code to stay working or fail with a compile error.

清单 15.5的情况正好相反。ICourse如果重新排列三个构造函数参数,代码将继续编译,ActivatorUtilities甚至可以构造一个新的ThreeCourseMeal. 但是除非清单 15.5根据重新排列进行更改,否则课程将以错误的顺序注入,这可能会导致应用程序行为不正确。不幸的是,没有重构工具会发出注册也必须更改的信号。

The opposite is true with listing 15.5. If the three ICourse constructor parameters are rearranged, code will keep compiling, and ActivatorUtilities would even be able to construct a new ThreeCourseMeal. But unless listing 15.5 is changed according to that rearrangement, the courses are injected in an incorrect order, which will likely cause the application to behave incorrectly. Unfortunately, no refactoring tool will signal that the registration must be changed too.

即使是 Autofac 和 Simple Injector(清单 13.7 和 14.9)的相关注册也能更好地防止错误。虽然这两个列表都不是类型安全的,但因为两个列表都匹配确切的参数名称,所以对 的更改ThreeCourseMeal至少会在解析类时导致异常。这总是比默默失败要好,在清单 15.5的情况下可能会发生这种情况。

Even the related registrations of Autofac and Simple Injector (listings 13.7 and 14.9) do a better job of preventing errors. Although neither listing is type-safe, because both listings match on exact parameter names, a change to the ThreeCourseMeal would at least cause an exception when the class is resolved. This is always better than failing silently, which is what could happen in the case of listing 15.5.

通过将参数显式映射到组件来覆盖自动装配是一种普遍适用的解决方案。在使用 Autofac 的命名注册和使用 MS.DI 的 Simple Injector 的条件注册的地方,您可以通过传递手动解析的具体类型来覆盖参数。如果您要管理许多类型,这可能会很脆弱。更好的解决方案是设计自己的 API 来消除这种歧义。它通常会带来更好的整体设计。

Overriding Auto-Wiring by explicitly mapping parameters to components is a universally applicable solution. Where you use named registrations with Autofac and conditional registrations with Simple Injector, with MS.DI, you override parameters by passing in manually resolved concrete types. This can be brittle if you have many types to manage. A better solution is to design your own API to get rid of that ambiguity. It often leads to a better overall design.

在下一节中,您将看到如何使用更明确、更灵活的方法,允许在一餐中提供任意数量的课程。为此,您必须了解 MS.DI 如何处理序列。

In the next section, you’ll see how to use the less ambiguous and more flexible approach where you allow any number of courses in a meal. To this end, you must learn how MS.DI deals with sequences.

15.4.2 接线顺序

15.4.2 Wiring sequences

在 6.1.1 节中,我们讨论了构造函数注入如何作为单一职责原则违规的警告系统。当时的教训是,而不是将构造函数过度注入视为构造函数注入模式的弱点,您应该庆幸它使有问题的设计如此明显。

In section 6.1.1, we discussed how Constructor Injection acts as a warning system for Single Responsibility Principle violations. The lesson then was that instead of viewing Constructor Over-injection as a weakness of the Constructor Injection pattern, you should rather rejoice that it makes problematic design so obvious.

当谈到DI 容器和歧义时,我们看到了类似的关系。DI 容器通常不会以优雅的方式处理歧义。虽然您可以让DI 容器处理它,但它看起来很尴尬。这通常表明您可以改进代码的设计。

When it comes to DI Containers and ambiguity, we see a similar relationship. DI Containers generally don’t deal with ambiguity in a graceful manner. Although you can make a DI Container deal with it, it can seem awkward. This is often an indication that you could improve the design of your code.

在本节中,我们将查看一个示例,演示如何通过重构消除歧义。它还将显示 MS.DI 如何处理序列。

In this section, we’ll look at an example that demonstrates how you can refactor away from ambiguity. It’ll also show how MS.DI deals with sequences.

通过消除歧义重构更好的课程

Refactoring to a better course by removing ambiguity

在 15.4.1 节中,您看到了ThreeCourseMeal及其固有的歧义如何迫使您要么放弃自动装配,要么使用对 的相当冗长的调用。一个简单的概括转向采用任意数量的实例而不是恰好三个实例的实现,就像类的情况一样:ActivatorUtilitiesIMealICourseThreeCourseMeal

In section 15.4.1, you saw how the ThreeCourseMeal and its inherent ambiguity forced you to either abandon Auto-Wiring or make use of the rather verbose call to ActivatorUtilities. A simple generalization moves toward an implementation of IMeal that takes an arbitrary number of ICourse instances instead of exactly three, as was the case with the ThreeCourseMeal class:

public Meal(IEnumerable<ICourse> courses)

请注意,不需要ICourse在构造函数中使用三个不同的实例,对一个实例的单一依赖IEnumerable<ICourse>可以让您为班级提供任意数量的课程Meal——从零到……很多!这解决了含糊不清的问题,因为现在只有一个Dependency。此外,它还通过提供一个单一的通用类来改进 API 和实现,该类可以模拟不同类型的膳食:从只有一道菜的简单膳食到精心制作的 12 道菜晚餐。

Notice that, instead of requiring three distinct ICourse instances in the constructor, the single dependency on an IEnumerable<ICourse> instance lets you provide any number of courses to the Meal class — from zero to ... a lot! This solves the issue with ambiguity, because there’s now only a single Dependency. In addition, it also improves the API and implementation by providing a single, general-purpose class that can model different types of meal: from a simple meal with a single course to an elaborate 12-course dinner.

在本节中,我们将了解如何配置 MS.DI 以Meal使用适当的ICourse Dependencies连接实例。完成后,您应该对需要使用Dependencies序列配置实例时可用的选项有一个很好的了解。

In this section, we’ll look at how you can configure MS.DI to wire up Meal instances with appropriate ICourse Dependencies. When we’re done, you should have a good idea of the options available when you need to configure instances with sequences of Dependencies.

自动接线序列

Auto-Wiring sequences

MS.DI 理解序列,所以如果你想使用给定服务的所有注册组件,自动装配就可以了。例如,您可以IMeal像这样配置服务及其课程:

MS.DI understands sequences, so if you want to use all registered components of a given service, Auto-Wiring just works. As an example, you can configure the IMeal service and its courses like this:

services.AddTransient<ICourse, Rillettes>();
services.AddTransient<ICourse, CordonBleu>();
services.AddTransient<ICourse, MousseAuChocolat>();

services.AddTransient<IMeal, Meal>();

请注意,这是从抽象到具体类型的完全标准映射。MS.DI 自动理解Meal构造函数并确定正确的操作过程是解析所有ICourse组件。当您解决时IMeal,您将获得一个包含组件、和的Meal实例。ICourseRillettesCordonBleuMousseAuChocolat

Notice that this is a completely standard mapping from Abstractions to concrete types. MS.DI automatically understands the Meal constructor and determines that the correct course of action is to resolve all ICourse components. When you resolve IMeal, you get a Meal instance with the ICourse components Rillettes, CordonBleu, and MousseAuChocolat.

MS.DI 自动处理序列,除非您另有指定,否则它会执行您期望的操作:它将一系列依赖项解析为该类型的所有已注册组件。只有当您需要明确地只从更大的集合中挑选一些组件时,您才需要做更多的事情。让我们看看如何做到这一点。

MS.DI automatically handles sequences, and unless you specify otherwise, it does what you’d expect it to do: it resolves a sequence of Dependencies to all registered components of that type. Only when you need to explicitly pick only some components from a larger set do you need to do more. Let’s see how you can do that.

从更大的集合中只挑选一些组件

Picking only some components from a larger set

MS.DI 注入所有组件的默认策略通常是正确的策略,但如图 15.4所示,可能存在您希望从所有已注册组件的较大集合中仅选择一些已注册组件的情况。

MS.DI’s default strategy of injecting all components is often the correct policy, but as figure 15.4 shows, there may be cases where you want to pick only some registered components from the larger set of all registered components.

15-04.eps

图 15.4 从更大的所有已注册组件集合中挑选组件

Figure 15.4 Picking components from a larger set of all registered components

当你之前让MS.DI Auto-Wire所有配置的实例时,就对应了图中右侧描述的情况。如果你想注册一个组件,如左侧所示,你必须明确定义应该使用哪些组件。为了实现这一点,您可以使用AddTransient接受委托的方法。这一次,您要处理的是Meal构造函数,它只接受一个参数。

When you previously let MS.DI Auto-Wire all configured instances, it corresponded to the situation depicted on the right side of the figure. If you want to register a component as shown on the left side, you must explicitly define which components should be used. In order to achieve this, you can use the AddTransient method that accepts a delegate. This time around, you’re dealing with the Meal constructor, which only takes a single parameter.

清单 15.6 将一个ICourse子集注入Meal

Listing 15.6 Injecting an ICourse subset into Meal

services.AddScoped<Rillettes>();    ①  
services.AddTransient<LobsterBisque>();    ①  
services.AddScoped<CordonBleu>();    ①  
services.AddScoped<OssoBuco>();    ①  
services.AddSingleton<MousseAuChocolat>();    ①  
services.AddTransient<CrèmeBrûlée>();    ①  

services.AddTransient<ICourse>(    ②  
    c => c.GetRequiredService<Rillettes>());    ②  
services.AddTransient<ICourse(    ②  
    c => c.GetRequiredService<LobsterBisque>());    ②  
services.AddTransient<ICourse>(    ②  
    c => c.GetRequiredService<CordonBleu>());    ②  
services.AddTransient<ICourse(    ②  
    c => c.GetRequiredService<OssoBuco>());    ②  
services.AddTransient<ICourse>(    ②  
    c => c.GetRequiredService<MousseAuChocolat>());  ②  
services.AddTransient<ICourse(    ②  
    c => c.GetRequiredService<CrèmeBrûlée>());    ②  

services.AddTransient<IMeal>(c = new Meal(
    new ICourse[]    ③  
    {    ③  
        c.GetRequiredService<Rillettes>(),    ③  
        c.GetRequiredService<CordonBleu>(),    ③  
        c.GetRequiredService<MousseAuChocolat>()    ③  
    }));    ③  

MS.DI 本身就理解序列;除非您需要明确地只从给定类型的所有服务中挑选一些组件,否则 MS.DI 会自动做正确的事情。自动接线不仅适用于单个实例,也适用于序列,并且容器将序列映射到相应类型的所有已配置实例。具有相同抽象的多个实例的一种可能不太直观的用法是装饰器设计模式,我们将在接下来讨论。

MS.DI natively understands sequences; unless you need to explicitly pick only some components from all services of a given type, MS.DI automatically does the right thing. Auto-Wiring works not only with single instances, but also for sequences, and the container maps a sequence to all configured instances of the corresponding type. A perhaps less intuitive use of having multiple instances of the same Abstraction is the Decorators design pattern, which we’ll discuss next.

15.4.3 接线装饰器

15.4.3 Wiring Decorators

在 9.1.1 节中,我们讨论了装饰器设计模式在实现横切关注点时如何发挥作用。根据定义,装饰器引入了相同抽象的多种类型。至少,您有两个抽象实现:装饰器本身和装饰类型。如果你堆叠装饰器,你可以拥有更多。这是对同一服务进行多次注册的另一个示例。与前面的部分不同,这些注册在概念上并不相等,而是彼此的依赖关系。

In section 9.1.1, we discussed how the Decorator design pattern is useful when implementing Cross-Cutting Concerns. By definition, Decorators introduce multiple types of the same Abstraction. At the very least, you have two implementations of an Abstraction: the Decorator itself and the decorated type. If you stack the Decorators, you can have even more. This is another example of having multiple registrations of the same service. Unlike the previous sections, these registrations aren’t conceptually equal, but rather Dependencies of each other.

装饰非泛型抽象

Decorating non-generic Abstractions

MS.DI 没有对装饰器的内置支持,这是 MS.DI 的局限性可能阻碍生产力的领域之一。尽管如此,我们将向您展示如何在某种程度上解决这些限制。

MS.DI has no built-in support for Decorators, and this is one of the areas where the limitations of MS.DI can hinder productivity. Nonetheless, we’ll show how you can, to some degree, work around these limitations.

您可以再次利用该ActivatorUtilities课程来绕过这个遗漏. 下面的例子展示了如何使用这个类来Breading申请VealCutlet

You can hack around this omission by, again, making use of the ActivatorUtilities class. The following example shows how to use this class to apply Breading to VealCutlet:

services.AddTransient<IIngredient>(c =>    ①  
    ActivatorUtilities.CreateInstance<Breading>(  ①  
        c,
        ActivatorUtilities    ②  
            .CreateInstance<VealCutlet>(c)));    ②  

正如你在第 9 章中学到的,当你在小牛肉排上切开一个口袋,然后在给小牛肉排裹上面包屑之前,将火腿、奶酪和大蒜放入口袋中,你就会得到小牛肉蓝带。下面的例子展示了如何在Decorator和DecoratorHamCheeseGarlic之间添加一个Decorator:VealCutletBreading

As you learned in chapter 9, you get veal cordon bleu when you slit open a pocket in the veal cutlet and add ham, cheese, and garlic into the pocket before breading the cutlet. The following example shows how to add a HamCheeseGarlic Decorator in between VealCutlet and the Breading Decorator:

services.AddTransient<IIngredient>(c =>
    ActivatorUtilities.CreateInstance<Breading>(
        c,
        ActivatorUtilities
            .CreateInstance<HamCheeseGarlic>(    ①  
            c,
            ActivatorUtilities
                .CreateInstance<VealCutlet>(c))));

通过使HamCheeseGarlic成为 的依赖BreadingVealCutlet依赖HamCheeseGarlicHamCheeseGarlic装饰器成为对象图中的中间类。这导致对象图等于以下纯 DI版本:

By making HamCheeseGarlic become a Dependency of Breading, and VealCutlet a Dependency of HamCheeseGarlic, the HamCheeseGarlic Decorator becomes the middle class in the object graph. This results in an object graph equal to the following Pure DI version:

new Breading(    ①  
    new HamCheeseGarlic(    ①  
        new VealCutlet()));    ①  

您可能会猜到,将装饰器与 MS.DI 链接起来既麻烦又冗长。让我们雪上加霜地看看如果您尝试将装饰器应用于通用抽象会发生什么。

As you might guess, chaining Decorators with MS.DI is cumbersome and verbose. Let’s add insult to injury by taking a look at what happens if you try to apply Decorators to generic Abstractions.

装饰通用抽象

Decorating generic Abstractions

在第 10 章中,我们定义了多个可以应用于任何ICommandService<TCommand>实现的通用装饰器。在本章的其余部分,我们将把我们的成分和课程放在一边,我们将看看如何使用 MS.DI 注册这些通用装饰器。以下清单演示了如何ICommandService<TCommand>使用 10.3 节中介绍的三个装饰器注册所有实现。

During the course of chapter 10, we defined multiple generic Decorators that could be applied to any ICommandService<TCommand> implementation. In the remainder of this chapter, we’ll set our ingredients and courses aside, and we’ll take a look at how to register these generic Decorators using MS.DI. The following listing demonstrates how to register all ICommandService<TCommand> implementations with the three Decorators presented in section 10.3.

清单 15.7 装饰通用的自动注册抽象

Listing 15.7 Decorating generic Auto-Registered Abstractions

Assembly assembly = typeof(AdjustInventoryService).Assembly;

var mappings =
    from type in assembly.GetTypes()    ①  
    where !type.IsAbstract    ①  
    where !type.IsGenericType    ①  
    from i in type.GetInterfaces()    ①  
    where i.IsGenericType    ①  
    where i.GetGenericTypeDefinition()    ①  
        == typeof(ICommandService<>)    ①  
    select new { service = i, implementation = type };  ①  

foreach (var mapping in mappings)
{
    Type commandType =    ②  
        mapping.service.GetGenericArguments()[0];    ②  

    Type secureDecoratoryType =    ③  
        typeof(SecureCommandServiceDecorator<>)    ③  
            .MakeGenericType(commandType);    ③  
    Type transactionDecoratorType =    ③  
        typeof(TransactionCommandServiceDecorator<>)    ③  
            .MakeGenericType(commandType);    ③  
    Type auditingDecoratorType =    ③  
        typeof(AuditingCommandServiceDecorator<>)    ③  
            .MakeGenericType(commandType);    ③  

    services.AddTransient(mapping.service, c =>    ④  
        ActivatorUtilities.CreateInstance(    ④  
            c,    ④  
            secureDecoratoryType,    ④  
            ActivatorUtilities.CreateInstance(    ④  
                c,    ④  
                transactionDecoratorType,    ④  
                ActivatorUtilities.CreateInstance(    ④  
                    c,    ④  
                    auditingDecoratorType,    ④  
                    ActivatorUtilities.CreateInstance(  ④  
                        c,    ④  
                        mapping.implementation)))));    ④  
}
15-05.eps

图 15.5 用事务、审计和安全方面丰富一个真正的命令服务

Figure 15.5 Enriching a real command service with transaction, auditing, and security aspects

清单 15.7的配置结果是图 15.5,我们之前在 10.3.4 节中讨论过。

The result of the configuration of listing 15.7 is figure 15.5, which we discussed previously in section 10.3.4.

如果您认为清单 15.7看起来相当复杂,不幸的是,这仅仅是个开始。该清单存在许多缺点,其中一些很难解决。其中包括:

In case you think that listing 15.7 looks rather complicated, unfortunately, this is just the beginning. That listing presents many shortcomings, some of which are difficult to work around. These include the following:

  • 当 Decorator 的任一泛型类型参数与Abstraction的参数不完全匹配时,创建封闭泛型 Decorator 类型会变得困难。4个 
  • Creation of closed-generic Decorator types can become difficult when either of the generic type arguments of the Decorator don’t exactly match that of the Abstraction.4 
  • 不可能添加应用装饰器的开放通用实现,而不必被迫显式地为每个封闭通用抽象进行注册。
  • It’s impossible to add open-generic implementations that get Decorators applied without being forced to explicitly make the registration for each closed-generic Abstraction.
  • 有条件地应用装饰器,例如,基于泛型类型参数,会变得很复杂。
  • Applying Decorators conditionally, for instance, based on generic type arguments, gets complicated.
  • 使用替代Lifestyle ,在实现实现多个接口的情况下防止 Torn Lifestyles变得复杂。
  • With an alternative Lifestyle, it becomes complex to prevent Torn Lifestyles in case an implementation implements multiple interfaces.
  • 很难区分生活方式;链中的所有装饰器都获得相同的Lifestyle
  • It’s hard to differentiate Lifestyles; all Decorators in the chain get the same Lifestyle.

您可以尝试一个接一个地解决这些限制并提出改进清单 15.7的建议,但是您实际上是在 MS.DI 之上开发一个新的DI 容器,这是我们不鼓励的。这不会有成效。Autofac 和 Simple Injector 等好的替代品更适合这种情况。5个 

You could try working through these limitations one-by-one and suggest improvements to listing 15.7, but you’d effectively be developing a new DI Container on top of MS.DI, which is something we discourage. This wouldn’t be productive. Good alternatives, such as Autofac and Simple Injector, are a better pick for this scenario.5 

尽管依赖依赖序列的消费者可以最直观地使用同一抽象的多个实例,但装饰器是另一个很好的例子。但是还有第三种可能有点令人惊讶的情况,其中多个实例开始发挥作用,这就是复合设计模式。

Although consumers that rely on sequences of Dependencies can be the most intuitive use of multiple instances of the same Abstraction, Decorators are another good example. But there’s a third and perhaps a bit surprising case where multiple instances come into play, which is the Composite design pattern.

15.4.4 布线复合材料

15.4.4 Wiring Composites

在本书的学习过程中,我们多次讨论了复合设计模式。例如,在 6.1.2 节中,您创建了一个(清单 6.4),它实现并包装了一系列实现。CompositeNotificationServiceINotificationServiceINotificationService

During the course of this book, we discussed the Composite design pattern on several occasions. In section 6.1.2, for instance, you created a CompositeNotificationService (listing 6.4) that both implemented INotificationService and wrapped a sequence of INotificationService implementations.

布线非通用复合材料

Wiring non-generic Composites

让我们看一下如何注册 Composites,例如MS.DI 中第 6 章的。下面的清单再次显示了这个类。CompositeNotification—Service

Let’s take a look at how you can register Composites, such as the CompositeNotification—Service of chapter 6 in MS.DI. The following listing shows this class again.

清单 15.8来自第 6 章 的CompositeNotificationServiceComposite

Listing 15.8 The CompositeNotificationService Composite from chapter 6

public class CompositeNotificationService : INotificationService
{
    private readonly IEnumerable<INotificationService> services;

    public CompositeNotificationService(
        IEnumerable<INotificationService> services)
    {
        this.services = services;
    }

    public void OrderApproved(Order order)
    {
        foreach (INotificationService service in this.services)
        {
            service.OrderApproved(order);
        }
    }
}

注册 Composite 需要将其添加为默认注册,同时将其注入一系列已解析的实例:

Registering a Composite requires it to be added as a default registration, while injecting it with a sequence of resolved instances:

services.AddTransient<OrderApprovedReceiptSender>();
services.AddTransient<AccountingNotifier>();
services.AddTransient<OrderFulfillment>();

services.AddTransient<INotificationService>(c =>
    new CompositeNotificationService(
        new INotificationService[]
        {
            c.GetRequiredService<OrderApprovedReceiptSender>(),
            c.GetRequiredService<AccountingNotifier>(),
            c.GetRequiredService<OrderFulfillment>(),
        }));

在此示例中,使用MS.DI的自动INotificationService装配 API按其具体类型注册了三个实现。另一方面,是使用委托注册的。在委托内部,手动更新 Composite 并注入一个实例数组。通过指定具体类型,解决了先前进行的注册。CompositeNotificationServiceINotificationService

In this example, three INotificationService implementations are registered by their concrete type using the Auto-Wiring API of MS.DI. The CompositeNotificationService, on the other hand, is registered using a delegate. Inside the delegate, the Composite is newed up manually and injected with an array of INotificationService instances. By specifying the concrete types, the previously made registrations are resolved.

由于通知服务的数量可能会随着时间的推移而增加,您可以通过应用自动注册来减轻组合根的负担。由于 MS.DI 在这方面缺乏任何功能,正如我们之前讨论的那样,您需要自己扫描程序集。

Because the number of notification services will likely grow over time, you can reduce the burden on your Composition Root by applying Auto-Registration. Because MS.DI lacks any features in this respect, as we discussed previously, you need to scan the assemblies yourself.

清单 15.9 注册CompositeNotificationService

Listing 15.9 Registering CompositeNotificationService

Assembly assembly = typeof(OrderFulfillment).Assembly;

Type[] types = (
    from type in assembly.GetTypes()
    where !type.IsAbstract
    where typeof(INotificationService).IsAssignableFrom(type)
    select type)
    .ToArray();    ①  

foreach (Type type in types)
{
    services.AddTransient(type);
}

services.AddTransient<INotificationService>(c =>
    new CompositeNotificationService(
        types.Select(t =>
            (INotificationService)c.GetRequiredService(t))
        .ToArray()));

清单 15.7的装饰器示例相比,清单 15.9看起来相当简单。扫描程序集的INotificationService实现,并将找到的每个类型附加到services集合中。类型数组由CompositeNotificationService注册使用。Composite 注入了一系列INotificationService实例,这些实例通过遍历类型数组来解析。

Compared to the Decorator example of listing 15.7, listing 15.9 looks reasonably simple. The assembly is scanned for INotificationService implementations, and each found type is appended to the services collection. The array of types is used by the CompositeNotificationService registration. The Composite is injected with a sequence of INotificationService instances that are resolved by iterating through the array of types.

您可能已经习惯了处理 MS.DI 时所需的复杂性和冗长程度,但不幸的是,我们还没有完成。我们的 LINQ 查询将注册任何实现INotificationService. 当您尝试运行前面的代码时,根据您的 Composite 所在的程序集,MS.DI 可能会抛出以下异常:

You might be getting used to the level of complexity and verbosity that you need when dealing with MS.DI, but unfortunately, we’re not done yet. Our LINQ query will register any non-generic implementation that implements INotificationService. When you try to run the previous code, depending on which assembly your Composite is located, MS.DI might throw the following exception:

引发了“System.StackOverflowException”类型的异常。

Exception of type 'System.StackOverflowException' was thrown.

哎哟! 堆栈溢出异常真的很痛苦,因为它们会中止正在运行的进程并且很难调试。此外,这个一般异常没有提供有关导致堆栈溢出的原因的详细信息。相反,您希望 MS.DI 抛出一个描述性异常来解释循环,就像 Autofac 和 Simple Injector 所做的那样。

Ouch! Stack overflow exceptions are really painful, because they abort the running process and are hard to debug. Besides, this generic exception gives no detailed information about what caused the stack overflow. Instead, you want MS.DI to throw a descriptive exception explaining the cycle, as both Autofac and Simple Injector do.

这个堆栈溢出异常由于CompositeNotificationService. Composite 由 LINQ 查询获取并作为序列的一部分解析。这导致 Composite 依赖于自身。这是一个 MS.DI 或任何DI 容器不可能构建的对象图。CompositeNotificationService成为序列的一部分,因为我们的 LINQ 查询找到了所有非泛型INotificationService实现,其中包括 Composite。

This stack overflow exception is caused by a cyclic Dependency in CompositeNotificationService. The Composite is picked up by the LINQ query and resolved as part of the sequence. This results in the Composite being dependent on itself. This is an object graph that’s impossible for MS.DI, or any DI Container for that matter, to construct. CompositeNotificationService became a part of the sequence because our LINQ query found all non-generic INotificationService implementations, which includes the Composite.

有多种解决方法。最简单的解决方案是将 Composite 移动到不同的程序集;例如,包含Composition Root的程序集。这可以防止 LINQ 查询从选择类型。另一种选择是CompositeNotificationService从列表中过滤掉:

There are multiple ways around this. The simplest solution is to move the Composite to a different assembly; for instance, the assembly containing the Composition Root. This prevents the LINQ query from selecting the type. Another option is to filter CompositeNotificationService out of the list:

Type[] types = (
    from type in assembly.GetTypes()
    where !type.IsAbstract
    where typeof(INotificationService)
        .IsAssignableFrom(type)
    where type != typeof(CompositeNotificationService)    ①  
    select type)
    .ToArray();

然而,复合类并不是唯一可能需要删除的类。您必须对任何 Decorator 执行相同的操作。这并不是特别困难,但是因为通常会有更多的 Decorator 实现,所以您最好查询类型信息以查明该类型是否表示 Decorator。以下是您也可以过滤掉装饰器的方法:

Composite classes, however, aren’t the only classes that might require removal. You’ll have to do the same for any Decorator. This isn’t particularly difficult, but because there typically will be more Decorator implementations, you might be better off querying the type information to find out whether the type represents a Decorator or not. Here’s how you can filter out Decorators as well:

Type[] types = (
    from type in assembly.GetTypes()
    where !type.IsAbstract
    where typeof(INotificationService).IsAssignableFrom(type)
    where type != typeof(CompositeNotificationService)
    where type => !IsDecoratorFor<INotificationService>(type)
    select type)
    .ToArray();

以下代码显示了该IsDecoratorFor方法:

And the following code shows the IsDecoratorFor method:

private static bool IsDecoratorFor<T>(Type type)
{
    return typeof(T).IsAssignableFrom(type) &&
        type.GetConstructors()[0].GetParameters()
            .Any(p => p.ParameterType == typeof(T));
}

IsDecoratorFor方法_期望一个类型只有一个构造函数。当一个类型既实现了给定的T 抽象,又当它的构造函数也需要一个T.

The IsDecoratorFor method expects a type to have only a single constructor. A type is considered to be a Decorator when it both implements the given T Abstraction and when its constructor also requires a T.

布线通用复合材料

Wiring generic Composites

在 15.4.3 节中,您看到了如何注册通用装饰器。在本节中,我们将看看如何为通用抽象注册 Composites 。

In section 15.4.3, you saw how to register generic Decorators. In this section, we’ll take a look at how you can register Composites for generic Abstractions.

在 6.1.3 节中,您指定了CompositeEventHandler<TEvent>(清单 6.12)作为一系列实现的复合IEventHandler<TEvent>实现。让我们看看您是否可以使用其包装的事件处理程序实现来注册 Composite。要在 MS.DI 中做到这一点,您必须发挥创造力,因为您必须解决一些不幸的限制。

In section 6.1.3, you specified the CompositeEventHandler<TEvent> class (listing 6.12) as a Composite implementation over a sequence of IEventHandler<TEvent> implementations. Let’s see if you can register the Composite with its wrapped event handler implementations. To pull this off in MS.DI, you’ll have to get creative, because you have to work around a few unfortunate limitations.

我们发现,将事件处理程序实现隐藏在 Composite 后面的最简单方法是根本不注册这些实现,而是将处理程序的构造移至 Composite。这不是很漂亮,但它完成了工作。为了将处理程序隐藏在 Composite 后面,您必须将CompositeEventHandler<TEvent>清单 6.12 的实现重写为清单 15.10中的实现。

We found that the easiest way to hide event handler implementations behind a Composite is by not registering those implementations at all, and instead moving the construction of the handlers to the Composite. This isn’t pretty, but it gets the job done. In order to hide handlers behind a Composite, you have to rewrite the CompositeEventHandler<TEvent> implementation of listing 6.12 to that in listing 15.10.

清单 15.10 MS.DI 兼容CompositeEventHandler<TEvent>实现

Listing 15.10 MS.DI–compatible CompositeEventHandler<TEvent> implementation

public class CompositeSettings    ①  
{    ①  
    public Type[] AllHandlerTypes { get; set; }    ①  
}    ①  

public class CompositeEventHandler<TEvent>
    : IEventHandler<TEvent>
{
    private readonly IServiceProvider provider;
    private readonly CompositeSettings settings;

    public CompositeEventHandler(
        IServiceProvider provider,    ②  
        CompositeSettings settings)    ②  
    {
        this.provider = provider;
        this.settings = settings;
    }

    public void Handle(TEvent e)
    {
        foreach (var handler in this.GetHandlers())    ③  
        {    ③  
            handler.Handle(e);    ③  
        }    ③  
    }

    IEnumerable<IEventHandler<TEvent>> GetHandlers()
    {
        return
            from type in this.settings.AllHandlerTypes
            where typeof(IEventHandler<TEvent>)    ④  
                .IsAssignableFrom(type)    ④  
            select (IEventHandler<TEvent>)
                ActivatorUtilities.CreateInstance(    ⑤  
                    this.provider, type);    ⑤  
    }
}

与清单 6.12 的原始实现相比,这个 Composite 实现更加复杂。它还通过使用其和. 鉴于这种依赖性,这个 Composite 肯定属于Composition Root内部,因为应用程序的其余部分应该不会使用DI ContainerIServiceProviderActivatorUtilities

Compared to the original implementation of listing 6.12, this Composite implementation is more complex. It also takes a hard dependency on MS.DI itself by making use of its IServiceProvider and ActivatorUtilities. In view of this dependency, this Composite certainly belongs inside the Composition Root, because the rest of the application should stay oblivious to the use of a DI Container.

Composite不依赖于序列,而是IEventHandler<TEvent>依赖于包含所有处理程序类型的参数对象,其中包括无法转换为IEventHandler<TEvent>Composite 的特定封闭泛型的类型。正因为如此,Composite 承担了DI Container应该做的部分工作。它通过调用过滤掉所有不兼容的类型typeof(IEventHandler<TEvent>).IsAssignableFrom(type)。这为您留下了 Composite 的注册和所有事件处理程序的扫描。

Instead of depending on an IEventHandler<TEvent> sequence, the Composite depends on a Parameter Object that contains all handler types, which includes types that can’t be cast to the specific closed-generic IEventHandler<TEvent> of the Composite. Because of this, the Composite takes on part of the job that the DI Container is supposed to do. It filters out all incompatible types by calling typeof(IEventHandler<TEvent>).IsAssignableFrom(type). This leaves you with a registration of the Composite and the scanning of all event handlers.

清单 15.11 注册CompositeEventHandler<TEvent>

Listing 15.11 Registering CompositeEventHandler<TEvent>

var handlerTypes =    ①  
    from type in assembly.GetTypes()    ①  
    where !type.IsAbstract    ①  
    where !type.IsGenericType    ①  
    let serviceTypes = type.GetInterfaces()    ①  
        .Where(i => i.IsGenericType &&    ①  
            i.GetGenericTypeDefinition()    ①  
                == typeof(IEventHandler<>))    ①  
    where serviceTypes.Any()    ①  
    select type;    ①  

services.AddSingleton(new CompositeSettings    ②  
{    ②  
    AllHandlerTypes = handlerTypes.ToArray()  ②  
});    ②  

services.AddTransient(    ③  
    typeof(IEventHandler<>),    ③  
    typeof(CompositeEventHandler<>));    ③  

与 fat Composite 实现一起,最后一个清单有效地实现了与 MS.DI 结合的 Composite 模式。

Together with the fat Composite implementation, this last listing effectively implements the Composite pattern in combination with MS.DI.

尽管我们已经设法解决了 MS.DI 的一些限制,但在其他情况下您可能就没那么幸运了。例如,如果元素序列同时包含非泛型和泛型实现,当泛型实现包含泛型类型约束或当装饰器需要有条件时,您可能会倒霉。

Even though we’ve managed to work around some of the limitations of MS.DI, you might be less lucky in other cases. For instance, you might run out of luck if the sequence of elements consists of both non-generic and generic implementations, when generic implementations contain generic type constraints or when Decorators need to be conditional.

我们确实承认这是一个令人不快的解决方案。我们更喜欢编写更少的代码来向您展示如何将 MS.DI 应用于本书中呈现的模式,但不幸的是,并非所有都是桃子和奶油。这就是为什么在我们的日常开发工作中,我们更喜欢Pure DI或成熟的DI 容器之一,例如 Autofac 和 Simple Injector。

We do admit that this is an unpleasant solution. We preferred writing less code to show you how to apply MS.DI to the patterns presented in this book, but not all is peaches and cream, unfortunately. That’s why, in our day-to-day development jobs, we prefer Pure DI or one of the mature DI Containers, such as Autofac and Simple Injector.

无论您选择哪种DI Container,或者即使您更喜欢Pure DI,我们都希望本书传达了一个重点——DI 不依赖于特定的技术,例如特定的DI Container。可以而且应该使用本书中介绍的 DI 友好模式和实践来设计应用程序。当您成功做到这一点时,DI 容器的选择就变得不那么重要了。DI 容器是一种组合您的应用程序的工具,但理想情况下,您应该能够用另一个容器替换一个容器,而无需重写应用程序的除组合根之外的任何部分。

No matter which DI Container you select, or even if you prefer Pure DI, we hope that this book has conveyed one important point — DI doesn’t rely on a particular technology, such as a particular DI Container. An application can, and should, be designed using the DI-friendly patterns and practices presented in this book. When you succeed in doing that, selection of a DI Container becomes of less importance. A DI Container is a tool that composes your application, but ideally, you should be able to replace one container with another without rewriting any part of your application other than the Composition Root.

概括

Summary

  • Microsoft.Extensions.DependencyInjection (MS.DI) DI 容器具有一组有限的功能。缺少解决Auto-Registration、Decorators 和 Composites 的综合 API。这使得它不太适合开发围绕本书中介绍的原则和模式设计的应用程序。
  • The Microsoft.Extensions.DependencyInjection (MS.DI) DI Container has a limited set of features. A comprehensive API that addresses Auto-Registration, Decorators, and Composites is missing. This makes it less suited for development of applications that are designed around the principles and patterns presented in this book.
  • MS.DI 在配置和使用容器之间强制执行严格的关注点分离。您使用ServiceCollection实例配置组件,但ServiceCollection无法解析组件。完成配置ServiceCollection后,您可以使用它来构建ServiceProvider可用于解析组件的 。
  • MS.DI enforces a strict separation of concerns between configuring and consuming a container. You configure components using a ServiceCollection instance, but a ServiceCollection can’t resolve components. When you’re done configuring a ServiceCollection, you use it to build a ServiceProvider that you can use to resolve components.
  • 使用 MS.DI,直接从根容器解析是一种不好的做法。这很容易导致内存泄漏或并发错误。相反,您应该始终从IServiceScope.
  • With MS.DI, resolving from the root container directly is a bad practice. This will easily lead to memory leaks or concurrency bugs. Instead, you should always resolve from an IServiceScope.
  • MS.DI 支持三种标准的LifestylesTransientSingletonScoped
  • MS.DI supports the three standard Lifestyles: Transient, Singleton, and Scoped.

词汇表

glossary

以下是本书中讨论的选定术语、模式和其他概念的简要定义。每个定义都包括对该术语进行更详细讨论的章节或部分的参考。

Here are brief definitions of selected terms, patterns, and other concepts discussed in this book. Each definition includes a reference to the chapter or section where the term is discussed in greater detail.

  • 抽象——一个包含接口和(抽象)基类的统一术语。见第 1 章。
  • Abstraction—A unifying term that encompasses both interfaces and (abstract) base classes. See chapter 1.
  • 环境上下文——一种 DI 反模式,它通过使用静态类成员为组合根外部的应用程序代码提供对易失性依赖项或其行为的全局访问。见第 5.3 节。
  • Ambient Context—A DI anti-pattern that supplies application code outside the Composition Root with global access to a Volatile Dependency or its behavior by the use of static class members. See section 5.3.
  • 面向方面的编程(AOP) - 一种软件方法,旨在减少实施横切关注点和其他编码模式所需的样板代码。它通过在一个地方实现此类模式并以声明方式或基于约定将它们应用于代码库来实现这一点,而无需修改代码本身。见第 10 章。
  • Aspect-Oriented Programming (AOP)—An approach to software that aims to reduce boilerplate code required for implementing Cross-Cutting Concerns and other coding patterns. It does this by implementing such patterns in a single place and applying them to a code base either declaratively or based on convention, without modifying the code itself. See chapter 10.
  • 自动注册——通过扫描一个或多个程序集以查找所需抽象的实现,根据DI 容器中的特定约定自动注册组件的能力。请参阅第 12.2.3 节。
  • Auto-Registration—The ability to automatically register components based on a certain convention in a DI Container by scanning one or more assemblies for implementations of desired Abstractions. See section 12.2.3.
  • 自动装配——利用编译器和公共语言运行时提供的类型信息,从抽象类型和具体类型之间的映射自动组成对象图的能力。请参阅第 12.1.2 节。
  • Auto-Wiring—The ability to automatically compose an object graph from maps between Abstractions and concrete types by making use of type information supplied by the compiler and the Common Language Runtime. See section 12.1.2.
  • Captive Dependency——一种无意中保持存活时间过长的依赖,因为其消费者的生命周期超过了Dependency预期生命周期。请参阅第 8.4.1 节。
  • Captive Dependency—A Dependency that’s inadvertently kept alive for too long, because its consumer was given a lifetime that exceeds the Dependency’s expected lifetime. See section 8.4.1.
  • 命令-查询分离——每个方法要么返回一个结果,但不改变系统的可观察状态,要么改变状态,但不产生任何值。请参阅第 10.3.3 节。
  • Command-Query Separation—The idea that each method should either return a result, but not change the observable state of the system, or change the state, but not produce any value. See section 10.3.3.
  • Composer——一个统一的术语,包含组成Dependencies的任何对象或方法。见第 8 章。
  • Composer—A unifying term that encompasses any object or method that composes Dependencies. See chapter 8.
  • Composition Root——应用程序的中心位置,整个应用程序由其组成模块组成。见第 4.1 节。
  • Composition Root—A central place in an application where the entire application is composed from its constituent modules. See section 4.1.
  • 配置为代码——允许将DI 容器的配置存储为源代码。抽象和特定实现之间的每个映射都在代码中明确和直接地表达。请参阅第 12.2.2 节。
  • Configuration as Code—Allows a DI Container’s configuration to be stored as source code. Each mapping between an Abstraction and a particular implementation is expressed explicitly and directly in code. See section 12.2.2.
  • Constrained Construction——一种 DI 反模式,它强制某个抽象的所有实现要求它们的构造函数具有相同的签名。见第 5.4 节。
  • Constrained Construction—A DI anti-pattern that forces all implementations of a certain Abstraction to require their constructors to have an identical signature. See section 5.4.
  • 构造函数注入——一种DI 模式,其中依赖项被静态定义为类构造函数的参数列表。参见第 4.2 节。
  • Constructor Injection—A DI pattern where Dependencies are statically defined as a list of parameters to the class’s constructor. See section 4.2.
  • Control Freak — 一种 DI 反模式,您在除Composition Root之外的任何地方都依赖于Volatile Dependency。它与控制反转相反。见第 5.1 节。
  • Control Freak—A DI anti-pattern where you depend on a Volatile Dependency in any place other than a Composition Root. It’s the opposite of Inversion of Control. See section 5.1.
  • 横切关注点——影响应用程序大部分的程序的一个方面。它通常是非功能性需求。典型示例包括日志记录、审计、访问控制和验证。见第 9 章。
  • Cross-Cutting Concern—An aspect of a program that affects a larger part of the application. It’s often a non-functional requirement. Typical examples include logging, auditing, access control, and validation. See chapter 9.
  • 依赖性——原则上,一个模块对另一个模块的任何引用。当一个模块引用另一个模块时,它依赖于它。非正式地,经常使用术语Dependency而不是更正式的Volatile Dependency。见第 1 章。
  • Dependency—In principle, any reference that a module holds to another module. When a module references another module, it depends on it. Informally, the term Dependency is often used instead of the more formal Volatile Dependency. See chapter 1.
  • 依赖倒置原则——该原则指出应用程序中的高层模块不应依赖于低层模块;相反,这两种类型都应该依赖于AbstractionsSOLID中的D。 _ 请参阅第 3.1.2 节。另见固体
  • Dependency Inversion Principle—This principle states that higher-level modules in your applications shouldn’t depend on lower-level modules; instead, both types should depend on Abstractions. The D in SOLID. See section 3.1.2. See also SOLID.
  • 依赖生命周期——见对象生命周期
  • Dependency Lifetime—See Object Lifetime.
  • DI 容器——一个软件库,它提供 DI 功能并自动执行对象组合拦截生命周期管理中涉及的许多任务。它是一个解析和管理对象图的引擎。见第 12 章。
  • DI Container—A software library that provides DI functionality and automates many of the tasks involved in Object Composition, Interception, and Lifetime Management. It’s an engine that resolves and manages object graphs. See chapter 12.
  • 实体——具有固有的、长期身份的域对象。请参阅第 3.1.2 节。
  • Entity—A domain object with an inherent, long-term identity. See section 3.1.2.
  • Foreign Default——在与消费者不同的模块中定义的Volatile Dependency的默认实现。请参阅第 5.1.3 节。
  • Foreign Default—A default implementation of a Volatile Dependency that’s defined in a different module than the consumer. See section 5.1.3.
  • 拦截——拦截两个协作组件之间的调用的能力,这样您就可以丰富或更改依赖项的行为,而无需更改两个协作者本身。见第 9 章。
  • Interception—The ability to intercept calls between two collaborating components in such a way that you can enrich or change the behavior of the Dependency without the need to change the two collaborators themselves. See chapter 9.
  • 接口隔离原则——该原则指出,不应强迫任何客户端依赖它不使用的方法。SOLID中的I。 _ 请参阅第 6.2.1 节。另见固体
  • Interface Segregation Principle—This principles states that no client should be forced to depend on methods it doesn’t use. The I in SOLID. See section 6.2.1. See also SOLID.
  • 控制反转——这个概念让框架控制对象的生命周期,而不是直接控制它们。见第 1 章。
  • Inversion of Control—This concept lets a framework control the lifetime of objects instead of directly controlling them. See chapter 1.
  • Leaky Abstraction——即使定义了抽象,实现细节也会暴露出来,从而将抽象锁定到实现中。请参阅第 6.2.1 节。
  • Leaky Abstraction—Even though an Abstraction is defined, the implementation details show through and thus lock the Abstraction to the implementation. See section 6.2.1.
  • Lifestyle——一种描述Dependency预期生命周期的形式化方式。见第 8 章。
  • Lifestyle—A formalized way of describing the intended lifetime of a Dependency. See chapter 8.
  • 生命周期管理——见对象生命周期
  • Lifetime Management—See Object Lifetime.
  • Liskov 替换原则——一种软件设计原则,规定消费者应该能够在不改变系统正确性的情况下使用抽象的任何实现。SOLID中的L。 _ 请参阅第 10.2.3 节。另见固体
  • Liskov Substitution Principle—A software design principle that states that a consumer should be able to use any implementation of an Abstraction without changing the correctness of the system. The L in SOLID. See section 10.2.3. See also SOLID.
  • Local Default——在与消费者相同的程序集中定义的抽象的默认实现。请参阅第 4.2.2 节。
  • Local Default—A default implementation of an Abstraction that’s defined in the same assembly as the consumer. See section 4.2.2.
  • 方法注入——一种DI 模式,其中依赖项作为方法参数注入到消费者中。见第 4.3 节。
  • Method Injection—A DI pattern where Dependencies are injected into the consumer as method parameters. See section 4.3.
  • 对象组合——从不同的模块组合应用程序的概念。见第 7 章。
  • Object Composition—The concept of composing applications from disparate modules. See chapter 7.
  • 对象生命周期——一般来说,这个术语涵盖了任何对象是如何创建和释放的。在 DI 上下文中,该术语涵盖Dependencies的生命周期。见第 8 章。
  • Object Lifetime—Generally speaking, this term covers how any object is created and deallocated. In DI context, this term covers the lifetime of Dependencies. See chapter 8.
  • 开放/封闭原则——该原则指出类应该对可扩展性开放,但对修改关闭。SOLID中的O。 _ 请参阅第 4.4.2 节。另见固体
  • Open/Closed Principle—This principle states that classes should be open for extensibility, but closed for modification. The O in SOLID. See section 4.4.2. See also SOLID.
  • 属性注入——一种DI 模式,其中依赖项通过可写属性注入到消费者中。见第 4.4 节。
  • Property Injection—A DI pattern where Dependencies are injected into the consumer via writable properties. See section 4.4.
  • 纯 DI — 在没有DI 容器的情况下应用 DI 的实践。见第 3 部分。
  • Pure DI—The practice of applying DI without a DI Container. See part 3.
  • Scoped Lifestyle——一种生活方式,在定义明确的范围或请求中只有一个实例,并且实例不跨范围共享。请参阅第 8.3.3 节。
  • Scoped Lifestyle—A Lifestyle where there’s a single instance within a well-defined scope or request, and instances aren’t shared across scopes. See section 8.3.3.
  • Seam——应用程序代码中的一个地方,抽象用于分隔模块。见第 1 章。
  • Seam—A place in application code where Abstractions are used to separate modules. See chapter 1.
  • 服务定位器——一种 DI 反模式,它为组合根之外的应用程序组件提供访问一组无限制的易失性依赖项的权限。参见第 5.2 节。
  • Service Locator—A DI anti-pattern that supplies application components outside the Composition Root with access to an unbounded set of Volatile Dependencies. See section 5.2.
  • Setter 注入— 请参阅属性注入
  • Setter Injection—See Property Injection.
  • 单一职责原则——这个原则指出一个类应该只有单一的职责。SOLID中的S。 _ 参见第 2.1.3 节。另见固体
  • Single Responsibility Principle—This principle states that a class should have only a single responsibility. The S in SOLID. See section 2.1.3. See also SOLID.
  • Singleton Lifestyle——一种生活方式,其中单个实例在单个Composer范围内为所有消费者重用。请参阅第 8.3.1 节。
  • Singleton Lifestyle—A Lifestyle where a single instance is reused for all consumers within the scope of a single Composer. See section 8.3.1.
  • SOLID — 代表五个基本设计原则的首字母缩写词:单一职责原则开放/封闭原则、里氏替换原则接口隔离原则依赖倒置原则。见第 10 章。
  • SOLID—An acronym that stands for five fundamental design principles: Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle. See chapter 10.
  • 稳定的依赖关系——可以被引用而没有任何不利影响的依赖关系。与Volatile Dependency相反。请参阅第 1.3.1 节。
  • Stable Dependency—A Dependency that can be referenced without any detrimental effects. The opposite of a Volatile Dependency. See section 1.3.1.
  • 时间耦合——当一个类的两个或多个成员之间存在隐式关系时会出现代码异味,要求客户先调用一个成员再调用另一个。请参阅第 4.3.2 节。
  • Temporal Coupling—Code smell that occurs when there’s an implicit relationship between two or more members of a class, requiring clients to invoke one member before the other. See section 4.3.2.
  • 可测试性——应用程序对自动化单元测试的敏感程度。见第 1 章。
  • Testability—The degree to which an application is susceptible to automated unit tests. See chapter 1.
  • Transient Lifestyle——一种所有消费者都有自己的Dependency实例的生活方式。请参阅第 8.3.2 节。
  • Transient Lifestyle—A Lifestyle where all consumers get their own instance of a Dependency. See section 8.3.2.
  • 易失性依赖性——一种涉及有时可能不受欢迎的副作用的依赖性。这可能包括尚不存在的模块或对其运行时环境有不利要求的模块。这些是 DI 解决的依赖关系。请参阅第 1.3.2 节。
  • Volatile Dependency—A Dependency that involves side effects that can be undesirable at times. This may include modules that don’t yet exist or that have adverse requirements on its runtime environment. These are the Dependencies that are addressed by DI. See section 1.3.2.

资源

resources

在印

In print

  • 博伊克,大卫。学习 NServiceBus,第 2 版。(Packt 出版社,2015 年)
  • Boike, David. Learning NServiceBus, 2nd Ed. (Packt Publishing, 2015)
  • 布朗、威廉 J. 等人。反模式:危机中的重构软件、架构和项目(威利计算机出版社,1998 年)
  • Brown, William J., et al. AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis (Wiley Computer Publishing, 1998)
  • 查特吉,阿扬。为通用 Windows 平台构建应用程序(Apress,2017 年)
  • Chatterjee, Ayan. Building Apps for the Universal Windows Platform (Apress, 2017)
  • Cwalina、Krzysztof 和 Brad Abrams,框架设计指南:可重用 .NET 库的约定、习语和模式,第 2 版。(艾迪生 - 卫斯理,2009)
  • Cwalina, Krzysztof and Brad Abrams, Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Ed. (Addison-Wesley, 2009)
  • 埃文斯,埃里克。领域驱动设计:解决软件核心的复杂性问题(Addison-Wesley,2004 年)
  • Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2004)
  • Feathers,Michael C.有效地使用遗留代码(Prentice Hall,2004 年)
  • Feathers, Michael C. Working Effectively with Legacy Code (Prentice Hall, 2004)
  • 福勒、马丁等人。重构:改进现有代码的设计(Addison-Wesley,1999)
  • Fowler, Martin, et al. Refactoring: Improving the Design of Existing Code (Addison-Wesley, 1999)
  • 福勒,马丁。企业应用架构模式(Addison-Wesley,2002 年)
  • Fowler, Martin. Patterns of Enterprise Application Architecture (Addison-Wesley, 2002)
  • 伽玛、埃里希等人。设计模式:可重用面向对象软件的要素(Addison-Wesley,1994 年)
  • Gamma, Erich, et al. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994)
  • Groves, Matthew D. .NET 中的 AOP(Manning,2013 年)
  • Groves, Matthew D. AOP in .NET (Manning, 2013)
  • 霍华德、迈克尔和大卫勒布朗。编写安全代码,第 2 版。(微软出版社,2003 年)
  • Howard, Michael and David LeBlanc. Writing Secure Code, 2nd Ed. (Microsoft Press, 2003)
  • 亨特、安迪和戴夫·托马斯。务实的程序员(Addison-Wesley,2000)
  • Hunt, Andy and Dave Thomas. The Pragmatic Programmer (Addison-Wesley, 2000)
  • 锁,安德鲁。ASP.NET Core 实战(Manning,2018 年)
  • Lock, Andrew. ASP.NET Core in Action (Manning, 2018)
  • 迈耶,伯特兰。面向对象的软件构建(ISE Inc.,1988)
  • Meyer, Bertrand. Object-Oriented Software Construction (ISE Inc., 1988)
  • 马丁、罗伯特 C. 等人。程序设计的模式语言 3(Addison-Wesley,1998)
  • Martin, Robert C., et al. Pattern Languages of Program Design 3 (Addison-Wesley, 1998)
  • Martin, Robert C.敏捷软件开发、原则、模式和实践(Prentice Hall,2003 年)
  • Martin, Robert C. Agile Software Development, Principles, Patterns, and Practices (Prentice Hall, 2003)
  • Martin, Robert C. Clean Code(Prentice Hall,2009 年)
  • Martin, Robert C. Clean Code (Prentice Hall, 2009)
  • 梅萨罗斯,杰拉德。xUnit 测试模式:重构测试代码(Addison-Wesley,2007 年)
  • Meszaros, Gerard. xUnit Test Patterns: Refactoring Test Code (Addison-Wesley, 2007)
  • Nygard, Michael T.释放它!设计和部署生产就绪软件(Pragmatic Bookshelf,2007 年)
  • Nygard, Michael T. Release It! Design and Deploy Production-Ready Software (Pragmatic Bookshelf, 2007)
  • 奥舍罗夫,罗伊。单元测试的艺术,第 2 版。(曼宁,2013 年)
  • Osherove, Roy. The Art of Unit Testing, 2nd Ed. (Manning, 2013)
  • 史密斯,乔恩。实体框架核心实战(Manning,2018 年)
  • Smith, Jon. Entity Framework Core in Action (Manning, 2018)

在线的

Online

其他资源

Other resources

指数

Index

符号

Symbols

.NET Core 控制台应用程序;net core 87

.NET Core console application;net core 87

.NET 事件;网络事件204

.NET events;net events 204

[CheckAuthorization]方面;CheckAuthorization 354

[CheckAuthorization] aspect;CheckAuthorization 354

[CircuitBreaker] 属性;CircuitBreaker 349

[CircuitBreaker] attribute;CircuitBreaker 349

[交易]属性;交易349

[Transaction] attribute;Transaction 349

@model 指令57

@model directive 57

一种

a

抽象类与接口6869

abstract classes vs. interfaces 6869

抽象工厂模式77 , 130130 , 191193

Abstract Factory pattern 77, 130130, 191193

抽象4 , 65 , 67

Abstractions 4, 65, 67

摘要关键字69

abstract keyword 69

ActionDescriptor 属性397 , 431

ActionDescriptor property 397, 431

激活器.CreateInstance 156 , 157

Activator.CreateInstance 156, 157

活化剂等级155

Activator class 155

ActivatorUtilities485486、487、489、497 _ _ _ _ _

ActivatorUtilities class 485486, 487, 489, 497

适配器设计模式13 , 72

Adapter design pattern 13, 72

AddProductCommand 属性223 , 225

AddProductCommand property 223, 225

添加产品方法223

AddProduct method 223

AddScoped 方法378 , 472 , 479

AddScoped method 378, 472, 479

AddSingleton<T>() 方法175

AddSingleton<T>() method 175

AddSingleton 方法231 , 378 , 472 , 479

AddSingleton method 231, 378, 472, 479

AddTransient方法378、472、479、482 _ _ _ _ _

AddTransient method 378, 472, 479, 482

AdjustInventory 命令321 , 323

AdjustInventory command 321, 323

调整库存服务323 , 324 , 335 , 335 , 401 , 436 , 475

AdjustInventoryService 323, 324, 335, 335, 401, 436, 475

调整库存视图模型321

AdjustInventoryViewModel 321

管理员角色331

Administrator role 331

ADO.NET 数据服务46

ADO.NET Data Services 46

AlertUser 方法298

AlertUser method 298

环境上下文反模式

Ambient Context anti-pattern

通过146148访问时间

accessing time through 146148

通过148153登录

logging through 148153

150151的负面影响

negative effects of 150151

重构 DI 151153

refactoring toward DI 151153

环境范围443444

ambient scopes 443444

歧义,消除

ambiguity, removing

重构417418 , 455455 , 487487

refactoring by 417418, 455455, 487487

使用 ActivatorUtilities 485486

using ActivatorUtilities 485486

使用代码块484485

using code blocks 484485

使用条件注册453454

using conditional registrations 453454

AOP(面向切面编程)31 , 31 , 302 , 302

AOP (Aspect-Oriented Programming) 31, 31, 302, 302

API(应用程序编程接口),注册409412 , 447451 , 480483

APIs (application programming interfaces), registering 409412, 447451, 480483

应用类227

App class 227

应用程序

applications

ApplyDiscountFor 方法104 , 108

ApplyDiscountFor method 104, 108

应用方法108 , 109

Apply method 108, 109

AsClosedTypesOf 401402

AsClosedTypesOf 401402

作为方法397 , 414

As method 397, 414

ASP.NET 核心框架

ASP.NET Core framework

IUserContext Adapter 特定于7172

IUserContext Adapter specific to 7172

MVC 应用程序,组成228234

MVC applications, composing 228234

在230231中使用自定义控制器激活器

using custom controller activators in 230231

ASP.NET Core Web 应用程序87

ASP.NET Core web application 87

ASP.NET Web 窗体98 , 213

ASP.NET Web Forms 98, 213

方面属性331 , 350

aspect attributes 331, 350

面向方面的编程

Aspect-Oriented Programming

AspNetUserContextAdapter类72、72、72、72、72、158、158、159、195、197、198、204 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

AspNetUserContextAdapter class 72, 72, 72, 72, 72, 158, 158, 159, 195, 197, 198, 204, 243, 243, 243, 260, 367, 370, 370, 371, 371, 375, 375

Assert.Equal 方法24

Assert.Equal method 24

Assert.Fail 方法312

Assert.Fail method 312

异步应用程序模型276278

asynchronous application models 276278

异步关键字233

async keyword 233

AsyncScopedLifestyle 444

AsyncScopedLifestyle 444

属性参数111

attribute parameter 111

审计

auditing

实施方面328331

implementing aspects 328331

实施审计追踪203

implementing audit trail 203

使用装饰器实现287290

implementing using Decorators 287290

AuditingCommandServiceDecorator<TCommand> 329

AuditingCommandServiceDecorator<TCommand> 329

AuditingUserRepositoryDe ​​corator288、288、288、289、289、290 _ _ _ _ _ _ _ _290 , 306 , 307 , 308 , 327

AuditingUserRepositoryDecorator class 288, 288, 288, 289, 289, 290290, 306, 307, 308, 327

授权334

authorization 334

Autofac.Core.KeyedService 424

Autofac.Core.KeyedService 424

Autofac 容器380

Autofac container 380

自动注册361 , 376 , 379 , 383 , 474

Auto-Registration 361, 376, 379, 383, 474

自动线容器365 , 367 , 371

AutoWireContainer 365, 367, 371

自动接线363364 , 364 , 371 , 410 , 413 , 415 , 417 , 430 , 453 , 489

Auto-Wiring 363364, 364, 371, 410, 413, 415, 417, 430, 453, 489

等待关键字233

await keyword 233

蔚蓝18 , 46

Azure 18, 46

AzureProductRepository 类132 , 132

AzureProductRepository class 132, 132

Azure 表数据访问层47

Azure Table data access layer 47

Azure 表服务76

Azure Table Service 76

B

BCL 基类库)16、26、44、65 _ _

BCL (Base Class Library) 16, 26, 44, 65

BeginLifetimeScope 方法407

BeginLifetimeScope method 407

BeginScope 方法441

BeginScope method 441

大对象图98

big object graphs 98

绑定标记扩展225

Binding markup extension 225

位图108

Bitmap 108

构建服务提供商469

BuildServiceProvider 469

总线系数386

bus factor 386

C

c

缓存291

caching 291

计算191 号公路

CalculateRoute 191

俘虏依赖110 , 266269 , 446446

Captive Dependencies 110, 266269, 446446

Castle.DynamicProxy.IInterceptor 接口344

Castle.DynamicProxy.IInterceptor interface 344

城堡.DynamicProxy.IInvocation 347

Castle.DynamicProxy.IInvocation 347

城堡动态代理344346

Castle Dynamic Proxy 344346

责任链模式287

Chain of Responsibility pattern 287

断路器属性353

CircuitBreakerAttribute 353

断路器等级295

CircuitBreaker class 295

断路器拦截器

CircuitBreakerInterceptor

使用 Pure DI 345346在 Composition Root 中应用

applying inside Composition Root using Pure DI 345346

实施344345

implementing 344345

断路器模式

Circuit Breaker pattern

为 IProductRepository 创建294295

creating for IProductRepository 294295

295297的实施

implementations of 295297

拦截292297

intercepting with 292297

CircuitBreakerProductRepositoryDe ​​corator294、296、302、308、342 _ _ _ _ _ _

CircuitBreakerProductRepositoryDecorator class 294, 296, 302, 308, 342

客户端应用程序7

client applications 7

关闭状态295

Closed state 295

CLR(公共语言运行时)57 , 348 , 363

CLR (Common Language Runtime) 57, 348, 363

代码块

code blocks

使用411412 , 450451 , 482483注册对象

registering objects with 411412, 450451, 482483

用484485消除歧义

removing ambiguity with 484485

代码凝聚力40

code cohesion 40

代码味道118 , 127 , 164 , 164

code smells 118, 127, 164, 164

凝聚力40 , 306

cohesion 40, 306

合作者1515

collaborators 1515

Collection.Create 方法456

Collection.Create method 456

收集.注册方法434 , 440

Collection.Register method 434, 440

命令-查询分离 (CQS) 315

Command-Query Separation (CQS) 315

命令315

commands 315

Commerce.Web 程序集375

Commerce.Web assembly 375

商业环境。数据库上下文203

CommerceContext. DbContext 203

CommerceContext类37、38、38、48、50、70、71、75、251、290_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

CommerceContext class 37, 38, 38, 48, 50, 70, 71, 75, 251, 290, 367

CommerceControllerActivator类90、90、90、91、231、231231、232、232、242、243、243、244、245、252、253、257、258 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

CommerceControllerActivator class 90, 90, 90, 91, 231, 231231, 232, 232, 242, 243, 243, 244, 245, 252, 253, 257, 258

公共语言运行时 (CLR) 57 , 348 , 363

Common Language Runtime (CLR) 57, 348, 363

通用服务定位器 (CSL) 138

Common Service Locator (CSL) 138

编译时间

compile time

编译时编织引起的耦合354354

coupling caused by compile-time weaving 354354

动态拦截失去支持347347

loss of support with dynamic Interception 347347

编译时支持347

compile-time support 347

编译时编织348355

compile-time weaving 348355

组成元素404

component elements 404

组件未注册异常 396

ComponentNotRegisteredException 396

组件

components

在 Autofac 中,多个

in Autofac, multiple

在多个候选人中选择413417

selecting among multiple candidates 413417

布线复合材料422425

wiring Composites 422425

接线装饰器420422

wiring Decorators 420422

接线顺序417420

wiring sequences 417420

在 MS.DI 中,多个483498

in MS.DI, multiple 483498

在 Simple Injector 中,多个451464

in Simple Injector, multiple 451464

7475人之间的互动

interaction between 7475

命名为416417

named 416417

释放406409 , 440443 , 477479

releasing 406409, 440443, 477479

从更大的集合中选择418420 , 455457 , 488489

selecting from larger set 418420, 455457, 488489

可组合性

composability

评估4547

evaluating 4547

缺失,分析4750

missing, analysis of 4750

作曲班242 , 242 , 264 , 264

Composer class 242, 242, 264, 264

复合设计模式13 , 170

Composite design pattern 13, 170

CompositeEventHandler < TEvent >类424、461、496

CompositeEventHandler<TEvent> class 424, 461, 496

复合记录器 275

CompositeLogger 275

CompositeNotificationService 171 , 172 , 172 , 177 , 422 , 423 , 459 , 459 , 492 , 493 , 493

CompositeNotificationService 171, 172, 172, 177, 422, 423, 459, 459, 492, 493, 493

复合图案

Composite pattern

通用,接线424425 , 461461 , 496498

generic, wiring 424425, 461461, 496498

通用,接线422424、459460、493495 _

non-generic, wiring 422424, 459460, 493495

接线422425 , 459461 , 492498

wiring 422425, 459461, 492498

复合包装177

composite wrapping 177

组成 根型60 , 68 , 8593

Composition Root pattern 60, 68, 8593

具体课程69

concrete classes 69

混凝土厂129130 , 130

Concrete Factory 129130, 130

混凝土类型396397 , 430431 , 469470

concrete types 396397, 430431, 469470

并发错误,通过将实例绑定到线程的生命周期275278

concurrency bugs, by tying instances to lifetime of threads 275278

有条件注册453454

conditional registrations 453454

配置

configuration

容器构建器 398404

ContainerBuilder 398404

容器432438

containers 432438

DI 容器372385

DI Containers 372385

实例范围405406

instance scopes 405406

生活方式439440 , 477477

Lifestyles 439440, 477477

同一服务的多个实施413414 , 452453 , 484484

multiple implementations of same service 413414, 452453, 484484

原始依赖409411 , 448449 , 480481

primitive Dependencies 409411, 448449, 480481

服务集合471476

ServiceCollection 471476

配置即代码

Configuration as Code

使用398400配置 ContainerBuilder

configuring ContainerBuilder with 398400

使用433434配置容器

configuring containers with 433434

使用377379配置 DI 容器

configuring DI Containers with 377379

使用472473配置 ServiceCollection

configuring ServiceCollection with 472473

配置文件

configuration files

使用403404配置 ContainerBuilder

configuring ContainerBuilder with 403404

使用437438配置容器

configuring containers with 437438

使用372376配置 DI 容器

configuring DI Containers with 372376

使用406406配置实例范围

configuring instance scopes with 406406

connectionString 参数367

connectionString parameter 367

控制台应用程序,构成213218

console applications, composing 213218

控制台类16

Console class 16

ConsoleMessageWriter类15、15、16、16、18、20、21、25、31 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

ConsoleMessageWriter class 15, 15, 16, 16, 18, 20, 21, 25, 31

ConsoleWriter 类20

ConsoleWriter class 20

约束构造模式19、98、153 – 160 _

Constrained Construction anti-pattern 19, 98, 153160

构造函数链133

Constructor Chaining 133

构造函数注入模式16 , 16 , 24 , 53 , 60 , 64 , 91 , 93 , 95102

Constructor Injection pattern 16, 16, 24, 53, 60, 64, 91, 93, 95102

构造函数过度注入代码气味164180

Constructor Over-injection code smell 164180

ConstructorResolutionBehavior 属性118 , 119 , 119 , 119

ConstructorResolutionBehavior property 118, 119, 119, 119

ContainerBuilder,配置398 -配置

ContainerBuilder, configuring 398404

容器类433

Container class 433

ContainerOptions 类119 , 119

ContainerOptions class 119, 119

容器

containers

配置432438

configuring 432438

检测俘虏依赖446446

detecting Captive Dependencies 446446

诊断终生问题444447

diagnosing for lifetime problems 444447

内容属性225

Content property 225

上下文参数111

context parameter 111

Control Freak模式88、97、127136、185 _ _ _

Control Freak anti-pattern 88, 97, 127136, 185

控制器激活器,自定义

controller activators, custom

创造230232

creating 230232

在 ASP.NET Core 中使用230231

using in ASP.NET Core 230231

控制器类42

Controller class 42

控制器42 , 231 , 362362

controllers 42, 231, 362362

ControllerTypeInfo 属性397 , 431

ControllerTypeInfo property 397, 431

controllerType 变量397 , 431 , 470

controllerType variable 397, 431, 470

转换器字段102

converter field 102

转换为方法112

ConvertTo method 112

编译时耦合354354

coupling at compile time 354354

CQS(命令-查询分离)315

CQS (Command-Query Separation) 315

创建、更新和删除 (CUD) 操作299

create, update, and delete (CUD) operations 299

CreateCurrencyParser方法215、216、216216、216、276、276 _ _ _ _ _ _ _ _ _

CreateCurrencyParser method 215, 216, 216216, 216, 276, 276

CreateHomeController 方法252

CreateHomeController method 252

CreateInstance<T> 方法485

CreateInstance<T> method 485

创建实例方法155

CreateInstance method 155

创建方法90 , 183 , 186 , 230 , 243

Create method 90, 183, 186, 230, 243

CreateNew 方法366

CreateNew method 366

创建页面方法228

CreatePage method 228

CreateRateDisplayer 方法264

CreateRateDisplayer method 264

CreateScope 方法478

CreateScope method 478

跨领域问题12、28、31、48、150、282、348 _ _ _ _ _ _ _ _ _ _ _

Cross-Cutting Concerns 12, 28, 31, 48, 150, 282, 348

交叉销售310

cross-sellings 310

CSL(通用服务定位器)138

CSL (Common Service Locator) 138

CUD(创建、更新和删除)操作299

CUD (create, update, and delete) operations 299

货币,更新214214

currencies, updating 214214

货币等级217

Currency class 217

货币兑换

currency conversions

添加到特色产品100102

adding to featured products 100102

添加到产品实体112113

adding to Product Entity 112113

货币监控程序262

CurrencyMonitoring program 262

货币解析器类216 , 216

CurrencyParser class 216, 216

货币属性102

Currency property 102

CurrencyRateDisplayer 类263 , 264

CurrencyRateDisplayer class 263, 264

现有房产146

Current property 146

当前用户属性197

CurrentUser property 197

客户服务类107

CustomerServices class 107

D

丹麦克朗符号58

Danish krone symbol 58

数据访问接口48 49

data access interface 4849

数据访问层4647 , 50 , 53 , 7071 , 76

data access layers 4647, 50, 53, 7071, 76

数据访问库47 , 48

data access library 47, 48

数据库引擎5

database engines 5

DataContext 属性220220225

DataContext property 220, 220, 225

数据层3639

data layers 3639

数据模板225

DataTemplate 225

数据传输对象 (DTO) 58 , 182

Data Transfer Objects (DTOs) 58, 182

日期时间方法146

DateTime method 146

DbContext.ChangeTracker 属性203

DbContext.ChangeTracker property 203

DbContext.SaveChanges 方法203

DbContext.SaveChanges method 203

DbContext类37、38、251、262265 _ _ _ _ _

DbContext class 37, 38, 251, 262265

DbContextOptionsBuilder 37

DbContextOptionsBuilder 37

DbSet<T> 类175

DbSet<T> class 175

DDD(领域驱动设计)105

DDD (Domain-Driven Design) 105

DDOS(分布式拒绝服务)292

DDOS (Distributed Denial of Service) 292

装饰器模式284287

Decorator pattern 284287

defaultAssembly 属性375 , 403

defaultAssembly attribute 375, 403

删除方法248 , 302

Delete method 248, 302

依赖关系

Dependencies

俘虏266269 , 446446

Captive 266269, 446446

105108的消费者

consumers of 105108

循环,固定194206

cyclic, fixing 194206

一次性245 255

disposable 245255

注入108109

injected 108109

注入 MainViewModel 220225

injecting into MainViewModel 220225

生命周期239242

lifecycles of 239242

命名为414416

named 414416

更新128128

newing up 128128

原始

primitive

配置409411 , 448449 , 480481

configuring 409411, 448449, 480481

提取到参数对象449450 , 481481

extracting to Parameter Objects 449450, 481481

释放250254255

releasing 250254255

基于运行时数据选择187193

selecting based on runtime data 187193

稳定2626

Stable 2626

挥发性2627

Volatile 2627

组合根9293

with Composition Root 9293

依赖图

dependency graphs

分析7578

analyzing 7578

评估4445

evaluating 4445

用于分析缺失的可组合性4748

for analysis of missing composability 4748

依赖倒置原则 (DIP) 66 , 304

Dependency Inversion Principle (DIP) 66, 304

依赖生命周期,管理238245

Dependency Lifetime, managing 238245

依赖属性114

Dependency property 114

部署神器87

deployment artifact 87

DI(依赖注入)

DI (Dependency Injection)

1724的优势

advantages of 1724

编译时编织方面对352354不友好

compile-time weaving aspects are unfriendly to 352354

关于58的神话

myths about 58

814的目的

purpose of 814

从 Ambient Context 重构到151153

refactoring from Ambient Context toward 151153

从 Constrained Construction 重构到157160

refactoring from Constrained Construction toward 157160

从 Control Freak 重构到135136

refactoring from Control Freak toward 135136

从服务定位器重构到144144

refactoring from Service Locator toward 144144

范围2731

scope 2731

3131的三个维度

three dimensions of 3131

诊断容器的寿命问题444447

diagnosing containers for lifetime problems 444447

诊断警告447

diagnostic warnings 447

去离子容器88 , 26 , 76 , 85 , 261

DI Containers 88, 26, 76, 85, 261

字典 <TKey, TValue> 类175

Dictionary<TKey, TValue> class 175

DIP(依赖倒置原则)676768 , 68 , 304 , 304

DIP (Dependency Inversion Principle) 676768, 68, 304, 304

DiscountedProduct 类别60 , 60

DiscountedProduct class 60, 60

DisplayRates 方法265

DisplayRates method 265

一次性依赖245255

disposable Dependencies 245255

一次性用品,临时247249

disposables, ephemeral 247249

一次性瞬态479

disposable Transients 479

处理方法442

Dispose method 442

分布式拒绝服务 (DDOS) 292

Distributed Denial of Service (DDOS) 292

领域驱动设计 (DDD) 105

Domain-Driven Design (DDD) 105

领域事件173180

domain events 173180

域层36 , 3942

domain layers 36, 3942

领域模型,独立,建筑6169

domain models, independent, building 6169

DoSomething 方法110

DoSomething method 110

DRY(不要重复自己)原则117、306、342

DRY (Don't Repeat Yourself) principle 117, 306, 342

DTO(数据传输对象)58 , 182

DTOs (Data Transfer Objects) 58, 182

动态拦截342348

dynamic Interception 342348

电子

e

电子商务应用程序,重建5374

e-commerce applications, rebuilding 5374

EditProductCommand属性223、224、225 _ _ _

EditProductCommand property 223, 224, 225

编辑产品方法223

EditProduct method 223

编辑产品视图模型248

EditProductViewModel 248

端点地址109

EndpointAddress 109

端点地址生成器109

EndpointAddressBuilder 109

实体63

Entity 63

实体框架核心38 , 70

Entity Framework Core 38, 70

枚举188

enum 188

临时一次性用品247 , 247248 , 249

ephemeral disposables 247, 247248, 249

相等法24

Equal method 24

错误处理140 , 291

error handling 140, 291

ErrorHandlingProductRepositoryDe ​​corator297、298

ErrorHandlingProductRepositoryDecorator class 297, 298

事件处理程序424

event handlers 424

例外,报告297298

exceptions, reporting 297298

感叹方法16 , 24

Exclaim method 16, 24

可执行文件87

executables 87

执行方法323

Execute method 323

延展性17 , 1920 , 48 , 118120

extensibility 17, 1920, 48, 118120

F

F

门面服务168173

Facade Services 168173

工厂,Control Freak 到129133

factories, Control Freak through 129133

容错291 , 328

fault tolerance 291, 328

恐惧、不确定和怀疑 (FUD) 4

fear, uncertainty, and doubt (FUD) 4

FeaturedProductsViewModel 类57 , 57

FeaturedProductsViewModel class 57, 57

反馈周期388389

feedback cycles 388389

最后声明253

finally statement 253

夹具拆解144

Fixture Teardown 144

折叠84

folding 84

外国违约97 , 133

Foreign Default 97, 133

正式迎宾员286

FormalGreeter 286

FUD(恐惧、不确定和怀疑)4

FUD (fear, uncertainty, and doubt) 4

G

g

通用库76

general-purpose library 76

通用子域361

Generic Subdomain 361

通用类型约束431

generic type constraints 431

GetAllInstances 方法462

GetAllInstances method 462

GetAll 方法295

GetAll method 295

获取属性适配器111

GetAttributeAdapter 111

GetById 方法289

GetById method 289

GetByName方法199、200、201、201 _ _ _ _ _

GetByName method 199, 200, 201, 201

GetCurrentScope 方法444

GetCurrentScope method 444

GetCurrentTime 方法148

GetCurrentTime method 148

GetFeaturedProducts方法47、48、60、61、64、75、101、112、113、113、128、141、185、270 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

GetFeaturedProducts method 47, 48, 60, 61, 64, 75, 101, 112, 113, 113, 128, 141, 185, 270

GetInstance 方法430 , 431 , 462

GetInstance method 430, 431, 462

GetLogger 方法151

GetLogger method 151

GetRequiredService方法470、470、470、476、484 _ _ _ _ _ _ _

GetRequiredService method 470, 470, 470, 476, 484

GetRoute 方法188

GetRoute method 188

GetService方法140、143、470 _ _ _

GetService method 140, 143, 470

GetTypesToRegister 方法435

GetTypesToRegister method 435

GetWelcomeMessage 方法147

GetWelcomeMessage method 147

返回方法223

GoBack method 223

神级165

God Class 165

上帝反对69

God Objects 69

问候方法285

Greet method 285

网格视图,XAML 224

GridView, XAML 224

保护条款16 , 59 , 119 , 140 , 167

Guard Clauses 16, 59, 119, 140, 167

守护方法294

Guard method 294

H

H

半开状态295

Half-Open state 295

处理方法176

Handle method 176

HasTierPrices 属性318

HasTierPrices property 318

你好迪!1424 , 28

Hello DI! 1424, 28

家庭控制器42、56、95、137、180、181、232、367 _ _ _ _ _ _ _ _ _ _ _ _

HomeController class 42, 56, 95, 137, 180, 181, 232, 367

HTTPContext 39

HttpContext 39

HttpContext.Response.RegisterForDispose 方法230 , 254

HttpContext.Response.RegisterForDispose method 230, 254

HttpContextAccessor 72

HttpContextAccessor 72

I

I/O 操作347

I/O operations 347

IApplicationContext 108

IApplicationContext 108

IAttributeAdapter 方法111 , 111

IAttributeAdapter method 111, 111

IAuditTrailAppender 接口195 , 195 , 198 , 200 , 201 , 205 , 287 , 288 , 289 , 306

IAuditTrailAppender interface 195, 195, 198, 200, 201, 205, 287, 288, 289, 306

计费系统166

IBillingSystem 166

ICircuitBreaker 接口295 , 296 , 303 , 353

ICircuitBreaker interface 295, 296, 303, 353

ICommandService 接口319 , 321323 , 326 , 327 , 382 , 401 , 436 , 475

ICommandService interface 319, 321323, 326, 327, 382, 401, 436, 475

IComponentContext 接口419

IComponentContext interface 419

IConstructorResolutionBehavior 接口118 , 450

IConstructorResolutionBehavior interface 118, 450

IControllerActivator 接口90 , 90 , 90 , 229 , 230 , 397 , 431

IControllerActivator interface 90, 90, 90, 229, 230, 397, 431

ICurrencyConverter.Exchange 方法112 , 113

ICurrencyConverter.Exchange method 112, 113

ICurrencyConverter 接口100 , 101 , 101 , 111 , 113 , 113 , 214 , 214

ICurrencyConverter interface 100, 101, 101, 111, 113, 113, 214, 214

ICustomerRepository 接口107

ICustomerRepository interface 107

ID一次性接口30 , 183 , 183 , 239 , 245

IDisposable interface 30, 183, 183, 239, 245

IEnumerable<T> 接口175 , 273275

IEnumerable<T> interface 175, 273275

IEventHandler<TEvent> 接口175 , 175 , 176

IEventHandler<TEvent> interface 175, 175, 176

IExchangeRateProvider 接口216 , 216

IExchangeRateProvider interface 216, 216

IGreeter 界面285

IGreeter interface 285

IHttpContext 接口182 , 182

IHttpContext interface 182, 182

身份20

IIdentity 20

IImageEffectAddIn 109

IImageEffectAddIn 109

库存管理166 , 168

IInventoryManagement 166, 168

IInvocation 接口345 , 345

IInvocation interface 345, 345

IL(中间语言)348

IL (Intermediate Language) 348

ILifestyleSelectionBehavior 440

ILifestyleSelectionBehavior 440

ILocationService 166 , 168

ILocationService 166, 168

IMessageService 166

IMessageService 166

IMessageWriter.Write 方法24

IMessageWriter.Write method 24

IMessageWriter 接口16 , 16 , 18 , 19 , 19 , 20 , 21 , 25

IMessageWriter interface 16, 16, 18, 19, 19, 20, 21, 25

实现属性438

implementation attribute 438

实现,拆分315317

implementations, splitting 315317

INavigationService.NavigateTo 方法222

INavigationService.NavigateTo method 222

独立领域模型,建筑6169

independent domain models, building 6169

指数法42 , 43

Index method 42, 43

索引视图标记43

Index view markup 43

初始化方法109 , 110 , 111 , 222

Initialize method 109, 110, 111, 222

注入流463

injected streams 463

INotificationService 170 , 171 , 173 , 179 , 422 , 459 , 492

INotificationService 170, 171, 173, 179, 422, 459, 492

INotifyPropertyChanged 接口222 , 223

INotifyPropertyChanged interface 222, 223

插入方法303

Insert method 303

InsertProduct 命令318

InsertProduct command 318

由内而外技术55

inside-out technique 55

InstancePerDependency 446

InstancePerDependency 446

实例范围属性406

instance-scope attribute 406

实例范围,配置405406

instance scopes, configuring 405406

拦截3031 , 62

Interception 3031, 62

拦截方法87 , 345

Intercept method 87, 345

拦截器

Interceptor

接口5

interfaces 5

接口隔离原则

Interface Segregation Principle

中间语言(IL)348

Intermediate Language (IL) 348

无效操作异常469

InvalidOperationException 469

库存控制器 322

InventoryController 322

调用方法233

Invoke method 233

IoC(控制反转29、76

IoC (Inversion of Control) 29, 76

IOrderFulfillment 接口168 , 168

IOrderFulfillment interface 168, 168

IOrderRepository 接口166 , 246 , 247

IOrderRepository interface 166, 246, 247

I主接口65 , 67

IPrincipal interface 65, 67

IProductCommandServices 接口315 , 315 , 336

IProductCommandServices interface 315, 315, 336

IProductManagementService 接口249

IProductManagementService interface 249

IProductQueryServices 接口336

IProductQueryServices interface 336

IProductRepositoryFactory 接口130 , 157 , 157 , 158 , 183 , 184 , 186

IProductRepositoryFactory interface 130, 157, 157, 158, 183, 184, 186

IProductRepository接口62、63、70、76、128、130、131、154、157、183、184、257、294295、297、346、367、369、381 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

IProductRepository interface 62, 63, 70, 76, 128, 130, 131, 154, 157, 183, 184, 257, 294295, 297, 346, 367, 369, 381

IProductService 接口59 , 309 , 348 , 367 , 436 , 475

IProductService interface 59, 309, 348, 367, 436, 475

IRouteAlgorithmFactory 接口189 , 189 , 189 , 191 , 192

IRouteAlgorithmFactory interface 189, 189, 189, 191, 192

IRouteAlgorithm 接口188 , 188 , 191

IRouteAlgorithm interface 188, 188, 191

IRouteCalculator 接口191

IRouteCalculator interface 191

isCustomerPreferred 参数40

isCustomerPreferred parameter 40

IsDecoratorFor 方法424 , 495

IsDecoratorFor method 424, 495

服务集合88

IServiceCollection 88

IServiceProvider 497

IServiceProvider 497

IServiceScope 469 , 477 , 482

IServiceScope 469, 477, 482

ISP(接口隔离原则)186 , 186 , 304 , 304

ISP (Interface Segregation Principle) 186, 186, 304, 304

项目点击命令224

ItemClickCommand 224

ITimeProvider 接口146 , 147 , 195 , 329

ITimeProvider interface 146, 147, 195, 329

ITypeDescriptorContext 111

ITypeDescriptorContext 111

IUIControlFactory 接口7

IUIControlFactory interface 7

IUpdateProductReviewTotalsService 318

IUpdateProductReviewTotalsService 318

IUserByNameRetriever 200 , 201

IUserByNameRetriever 200, 201

IUserContext 适配器7172

IUserContext Adapter 7172

IUserContext 接口65 , 67 , 71 , 101 , 158 , 183 , 183 , 198 , 200 , 367

IUserContext interface 65, 67, 71, 101, 158, 183, 183, 198, 200, 367

IUserRepository 200 , 201 , 288 , 288 , 306

IUserRepository 200, 201, 288, 288, 306

IValidationAttributeAdapterProvider 接口111

IValidationAttributeAdapterProvider interface 111

IViewModel 接口223

IViewModel interface 223

IVoucherRedemptionService 107 , 107

IVoucherRedemptionService 107, 107

J

JIT(即时)编译器348

JIT (Just-In-Time) compiler 348

正当理由447

justification argument 447

K

KeyedService 类424

KeyedService class 424

KeyNotFoundException 141

KeyNotFoundException 141

键值数据库46

key-value database 46

l

拉姆达表达式369

lambda expression 369

后期绑定5617 , 18 , 19 , 29 , 48 , 154156 , 377

late binding 5617, 18, 19, 29, 48, 154156, 377

在 UpdateCurrency 217218中分层

layering in UpdateCurrency 217218

懒惰<T> 269272

Lazy<T> 269272

LazyAuditTrailAppender 205 , 206

LazyAuditTrailAppender 205, 206

惰性用户上下文代理272 , 272

LazyUserContextProxy 272, 272

泄漏的抽象

Leaky Abstractions

作为无参数工厂方法183184

as parameterless factory methods 183184

代码气味182183

code smells 182183

IEnumerable<T> 为273275

IEnumerable<T> as 273275

惰性 <T> 为269272

Lazy<T> as 269272

向消费者泄露生活方式选择269275

to leak Lifestyle choices to consumers 269275

遗留应用程序21

legacy applications 21

图书馆

libraries

可重用69

resuable 69

第三方386387

third-party 386387

依赖的生命周期239239240 , 242

lifecycles of Dependencies 239239240, 242

Lifestyle.Singleton 和 Lifestyle.Transient 442

Lifestyle.Singleton and Lifestyle.Transient 442

生活方式

Lifestyles

糟糕的选择266278

bad choices 266278

配置439440 , 477477

configuring 439440, 477477

图案255265

patterns 255265

终身管理30 , 183 , 185 , 404409 , 438447 , 476479

Lifetime Management 30, 183, 185, 404409, 438447, 476479

生命周期范围406

lifetime scopes 406

LINQ 查询495

LINQ query 495

Liskov 替换原则10 , 21 , 30 , 241 , 283

Liskov Substitution Principle 10, 21, 30, 241, 283

本地默认值97 , 101 , 114

Local Default 97, 101, 114

定位器.Reset() 方法144

Locator.Reset() method 144

定位器等级140 , 143

Locator class 140, 143

记录76 , 102 , 149 , 328 , 330

logging 76, 102, 149, 328, 330

记录中间件类233

LoggingMiddleware class 233

逻辑

logic

实施应用程序逻辑1516

implementing application logic 1516

单元测试2324

unit testing 2324

逻辑工件87

logical artifacts 87

LogManager.GetLogger 方法151

LogManager.GetLogger method 151

日志方法275

Log method 275

长时间运行的应用程序262265

long-running applications 262265

松耦合5 , 26 , 283

loose coupling 5, 26, 283

松散耦合的代码

loosely coupled code

分析松散耦合的实现7478

analyzing loosely coupled implementations 7478

重建电子商务应用程序5374

rebuilding e-commerce applications 5374

LSP(里氏替换原则)

LSP (Liskov Substitution Principle)

分析意外违规323324

analyzing accidental violations 323324

使用通用抽象进行修复324326

fixing using generic Abstraction 324326

M

主要方法15 , 15 , 28 , 29 , 86 , 87 , 88 , 213

Main method 15, 15, 28, 29, 86, 87, 88, 213

可维护性17 , 2121 , 48

maintainability 17, 2121, 48

主视图模型类

MainViewModel class

将依赖项注入220225

injecting Dependencies into 220225

接线225225

wiring up 225225

MakePreferred 方法106

MakePreferred method 106

将抽象映射到具体类型396397 , 469470

mapping Abstractions to concrete types 396397, 469470

消息,拦截21

messages, intercepting 21

messageWriter 应用程序19

messageWriter application 19

短信179

messaging 179

方法调用

method calls

每个105108的不同依赖消费者

varying Dependency consumer on each 105108

每个108109的不同注入依赖

varying injected Dependency on each 108109

MethodExecutionTag 属性351

MethodExecutionTag property 351

方法 注入模式53 , 104113

Method Injection pattern 53, 104113

Microsoft.Extensions.DependencyInjection 378 ,

Microsoft.Extensions.DependencyInjection 378,

微软 Azure 46

Microsoft Azure 46

Microsoft 分布式事务处理协调器 (MSDTC) 322

Microsoft Distributed Transaction Coordinator (MSDTC) 322

中间件233234

middleware 233234

模型属性57 , 225

Model property 57, 225

MS.DI (Microsoft.Extensions.DependencyInjection)

MS.DI (Microsoft.Extensions.DependencyInjection)

终身管理476479

Lifetime Management 476479

467476概述

overview of 467476

注册困难的 API 480483

registering difficult APIs 480483

使用多个组件483498

working with multiple components 483498

MSDTC(微软分布式事务处理协调器)322

MSDTC (Microsoft Distributed Transaction Coordinator) 322

多线程276278

multi-threading 276278

MVC(模型视图控制器)应用程序,用于 ASP.NET Core 228234

MVC (Model View Controller) applications, for ASP.NET Core 228234

MVVM(模型-视图-视图模型218、218、220、220

MVVM (Model-View-ViewModel) 218, 218, 220, 220

MyApp.Services.Products 命名空间323

MyApp.Services.Products namespace 323

N

命名方法414

Named method 414

导航方法225

Navigate method 225

NavigateTo 方法222 , 222

NavigateTo method 222, 222

新关键字50 , 127 , 127 , 134

new keyword 50, 127, 127, 134

新产品视图模型223

NewProductViewModel 223

n层应用35 , 60

n-layer application 35, 60

没有操作117

no operation 117

NuGet 包441

NuGet packages 441

空参数96

null argument 96

空对象模式102 , 117

Null Object pattern 102, 117

引用异常11、115、293、353 _ _ _ _

NullReferenceException 11, 115, 293, 353

空值141

null value 141

O

对象组合28 , 2929 , 91 , 212 , 385

Object Composition 28, 2929, 91, 212, 385

对象图72 , 72 , 98

object graphs 72, 72, 98

对象生命周期3030

Object Lifetime 3030

对象

objects

具体类型的抽象430431

Abstractions to concrete types 430431

解析395397 , 429432 , 468471

resolving 395397, 429432, 468471

使用代码块,注册411412 , 450451 , 482483

with code blocks, registering 411412, 450451, 482483

OCP(开闭原则)

OCP (Open/Closed Principle)

使用参数对象317321修复

fixing using Parameter Objects 317321

IProductService 违反了312313

IProductService violates 312313

OnLaunched 方法219

OnLaunched method 219

OnMethodBoundaryAspect 350 , 351

OnMethodBoundaryAspect 350, 351

OnSuccess 方法351

OnSuccess method 351

开闭原则14 , 21 , 116 , 200 , 297 , 410

Open/Closed Principle 14, 21, 116, 200, 297, 410

打开状态295

Open state 295

可选方法参数222

optional method arguments 222

选项属性119

Options property 119

OrderAccepted 事件179

OrderAccepted event 179

OrderApproved 类174

OrderApproved class 174

OrderCancelled 类174

OrderCancelled class 174

OrderFulfillment 类175 , 176

OrderFulfillment class 175, 176

订单服务类167168169246

OrderService class 167, 168, 169, 246

由外而内技术55

outside-in technique 55

重载构造函数133134

overloaded constructors 133134

p

p

并行开发17 , 2021 , 49

parallel development 17, 2021, 49

无参数工厂方法183184

parameterless factory methods 183184

参数对象

Parameter Objects

提取原始依赖关系到449450 , 481481

extracting primitive Dependencies to 449450, 481481

使用317321修复 OCP

fixing OCP using 317321

被动属性331

passive attributes 331

模式,选择120121

patterns, choosing 120121

性能监控328

performance monitoring 328

PermittedRoleAttribute 332、332、333 _ _ _ _

PermittedRoleAttribute 332, 332, 333

执着无知70

persistence ignorance 70

每线程生活方式266

per-thread Lifestyle 266

POCO普通旧CLR对象57、57、220、220

POCOs (Plain Old CLR Objects) 57, 57, 220, 220

政策后缀401

Policy suffix 401

PostSharp 工具350

PostSharp tool 350

PowerShell 213

PowerShell 213

谓词值453

predicate value 453

价格值参数105

price value parameter 105

原始依赖

primitive Dependencies

配置409411 , 480481

configuring 409411, 480481

提取到参数对象481481

extracting to Parameter Objects 481481

私有只读字段151

private readonly field 151

继续方法345

Proceed method 345

产品类别37 , 50 , 63 , 70

Product class 37, 50, 63, 70

产品实体112113

Product Entity 112113

产品管理富客户,接线219225

product-management rich clients, wiring up 219225

产品资料库239

productRepository 239

产品资料库75 , 154156

ProductRepository 75, 154156

ProductRepositoryFactory类131、131、132、132 _ _ _ _ _

ProductRepositoryFactory class 131, 131, 132, 132

ProductRepositoryStub 141

ProductRepositoryStub 141

产品服务等级40、40、42、47、64、72、86、101、101、111、128、129、133、140 – 142、143、239、243、260、272 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

ProductService class 40, 40, 42, 47, 64, 72, 86, 101, 101, 111, 128, 129, 133, 140142, 143, 239, 243, 260, 272

ProductViewModel 类57

ProductViewModel class 57

程序类87 , 213 , 264

Program class 87, 213, 264

属性注入模式93、102、114120、204、204、205 _ _ _ _ _ _ _ _

Property Injection pattern 93, 102, 114120, 204, 204, 205

公共关键字69

public keyword 69

纯去离子15 , 88 , 144 , 216 , 376

Pure DI 15, 88, 144, 216, 376

r

r

只读字段144

readonly field 144

只读关键字245

readonly keyword 245

读取,与写入分开314315

reads, separating from writes 314315

RedeemVoucher 方法106 , 107

RedeemVoucher method 106, 107

重构

refactoring

通过消除歧义417418 , 455455 , 487487

by removing ambiguity 417418, 455455, 487487

从抽象工厂到适配器191193

from Abstract Factory to Adapter 191193

从环境语境到 DI 151153

from Ambient Context toward DI 151153

从约束结构到 DI 157160

from Constrained Construction toward DI 157160

从构造函数过度注入到领域事件173180

from Constructor Over-injection to domain events 173180

从构造器过度注入到外观服务168173

from Constructor Over-injection to Facade Services 168173

从 Control Freak 到 DI 135136

from Control Freak toward DI 135136

从服务定位器到 DI 144144

from Service Locator toward DI 144144

从违反 SRP 到解决依赖周期200203

from SRP violations to resolve Dependency cycles 200203

RegisterAssemblyTypes方法400、401、402、421、423 _ _ _ _ _ _ _

RegisterAssemblyTypes method 400, 401, 402, 421, 423

RegisterConditional 方法453

RegisterConditional method 453

RegisterDecorator 383 , 420421 , 457 , 461

RegisterDecorator 383, 420421, 457, 461

注册组件456

registered components 456

RegisterGenericDecorator 420 , 421422 , 424

RegisterGenericDecorator 420, 421422, 424

注册方法141 , 365 , 411

Register method 141, 365, 411

注册解决发布模式372

Register Resolve Release pattern 372

RegisterType 方法397 , 398

RegisterType method 397, 398

注册

registrations

有条件的 453454

conditional 453454

困难的 API 409412 , 447451 , 480483

difficult APIs 409412, 447451, 480483

命名依赖项 414416

named Dependencies 414416

代码块为411412450451482483的对象

objects with code blocks 411412, 450451, 482483

抑制对个人447447的警告

suppressing warnings on individual 447447

释放方法230 , 253

Release method 230, 253

释放组件406409 , 440443 , 477479

releasing components 406409, 440443, 477479

可靠的消息传递179 , 179

reliable messaging 179, 179

存储库模式

Repository pattern

解析多个259260

resolving multiple 259260

内存中的线程安全257258

thread-safe in-memory 257258

存储库变量128 , 128 , 241

repository variable 128, 128, 241

重置方法141

Reset method 141

解决 <T> 方法397

Resolve<T> method 397

解决 API 361363

Resolve API 361363

ResolvedParameter类415

ResolvedParameter class 415

解析方法88 , 362 , 362 , 368

Resolve method 88, 362, 362, 368

解决394404

resolving 394404

可重复使用的库69 , 118120

reusable libraries 69, 118120

基于角色的安全性334

role-based security 334

路线计算器类192 , 243

RouteCalculator class 192, 243

路由控制器188 , 189 , 191 , 232

RouteController 188, 189, 191, 232

路线类型188

RouteType 188

基于行的安全性334

row-based security 334

runtime数据,根据187-193选择Dependencies

runtime data, selecting Dependencies based on 187193

小号

S

称呼类15 , 15 , 16 , 16 , 19 , 23 , 24 , 25

Salutation class 15, 15, 16, 16, 19, 23, 24, 25

作用域 DbContext 262265

scoped DbContext 262265

作用域依赖261 , 278

Scoped Dependencies 261, 278

范围 生活方式模式260265

Scoped Lifestyle pattern 260265

范围,环境443444

scopes, ambient 443444

接缝25 , 213 , 218

Seams 25, 213, 218

SecureMessageWriter类20、20、20、21、31、287 _ _ _ _ _ _ _ _ _

SecureMessageWriter class 20, 20, 20, 21, 31, 287

SecureProductRepositoryDe ​​corator 299、308、331 _ _ _

SecureProductRepositoryDecorator 299, 308, 331

保安19 , 291 , 328 , 331334

security 19, 291, 328, 331334

敏感功能298300

sensitive functionality 298300

关注点分离40 , 50

separation of concerns 40, 50

序列

sequences

自动接线418418 , 455455 , 487487

Auto-Wiring 418418, 455455, 487487

流为462464

streams as 462464

接线417420 , 454457 , 486489

wiring 417420, 454457, 486489

ServiceCollection,配置471 - 476

ServiceCollection, configuring 471476

服务定位器反模式7、7、8、88、88、136144、353 _ _ _ _ _ _ _ _

Service Locator anti-pattern 7, 7, 8, 88, 88, 136144, 353

ServiceProvider 属性467 , 478

ServiceProvider property 467, 478

服务,弱类型397397 , 431432 , 470471

services, weakly typed 397397, 431432, 470471

SimpleDecorator类285 , 286

SimpleDecorator class 285, 286

简易喷油器

Simple Injector

终身管理438447

Lifetime Management 438447

428438概述

overview of 428438

注册困难的 API 447451

registering difficult APIs 447451

使用多个组件451464

working with multiple components 451464

SimpleInjector.Contain 430

SimpleInjector.Containe 430

单一实例方法405

SingleInstance method 405

单一职责原则

Single Responsibility Principle

单例依赖261

Singleton Dependencies 261

单身人士生活方式模式242 , 256258 , 439

Singleton Lifestyle pattern 242, 256258, 439

单身人士245

Singletons 245

场地物业118

Site property 118

坚硬的

SOLID

从311-313角度分析IProductService

analysis of IProductService from perspective of 311313

应用原则改进设计314327

applying principles to improve design 314327

作为 AOP 308337的驱动程序

as driver for AOP 308337

304308原则

principles for 304308

分班204

split classes 204

分裂

splitting

实施315317

implementations 315317

接口315317

interfaces 315317

间谍消息编写器24

SpyMessageWriter 24

SqlAuditTrailAppender 195、195、198、204、204、205 _ _ _ _ _ _ _ _ _ _

SqlAuditTrailAppender 195, 195, 198, 204, 204, 205

SqlExchangeRateProvider 216 , 276

SqlExchangeRateProvider 216, 276

SqlProductRepository 66、67、72、128、132、132、133、158、158、158、185、185、239、243、243、283、350、367、370、381 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

SqlProductRepository 66, 67, 72, 128, 132, 132, 133, 158, 158, 158, 185, 185, 239, 243, 243, 283, 350, 367, 370, 381

SqlProductRepositoryFactory 158、159、159、159 _ _ _ _ _ _

SqlProductRepositoryFactory 158, 159, 159, 159

SqlProductRepositoryProxy 186 , 186 , 186

SqlProductRepositoryProxy 186, 186, 186

SqlUserByNameRetriever 201

SqlUserByNameRetriever 201

SqlUserRepository类197、199、199、201、204、260 _ _ _ _ _ _ _ _ _

SqlUserRepository class 197, 199, 199, 201, 204, 260

SRP(单一职责原则)21 , 28 , 31 , 40 , 40 , 40 , 50 , 165 , 165

SRP (Single Responsibility Principle) 21, 28, 31, 40, 40, 40, 50, 165, 165

稳定的依赖关系2626

Stable Dependencies 2626

StackOverflowExceptions 199 , 371异常

StackOverflowExceptions 199, 371

Startup.ConfigureServices 方法90

Startup.ConfigureServices method 90

创业班88 , 88 , 230 , 230 , 233 , 245

Startup class 88, 88, 230, 230, 233, 245

无状态服务259

stateless service 259

静态工厂131133

Static Factory 131133

字符串参数154

string argument 154

SummaryText 属性57

SummaryText property 57

抑制诊断警告 447

SuppressDiagnosticWarning 447

SUT(被测系统)21

SUT (System Under Test) 21

System.Activator 类366

System.Activator class 366

系统.ComponentModel.DataAnnotations 328

System.ComponentModel.DataAnnotations 328

系统.ComponentModel.Design.IDesigner 111

System.ComponentModel.Design.IDesigner 111

System.ComponentModel 命名空间111

System.ComponentModel namespace 111

系统.Data.SqlClient.SqlDataReader 462

System.Data.SqlClient.SqlDataReader 462

系统.DateTime.Now 27

System.DateTime.Now 27

System.IO.StreamReader 类99

System.IO.StreamReader class 99

System.IO.StreamWriter类99

System.IO.StreamWriter class 99

System.Lazy<T> 类269

System.Lazy<T> class 269

系统.随机27

System.Random 27

System.Security.Cryptography.RandomNumberGenerator 27

System.Security.Cryptography.RandomNumberGenerator 27

System.Security.Principal.WindowsIdentity 类20

System.Security.Principal.WindowsIdentity class 20

System.Timers.Timer 类265

System.Timers.Timer class 265

System.Transactions.TransactionScope 类322

System.Transactions.TransactionScope class 322

系统.Windows.Input.ICommand 220

System.Windows.Input.ICommand 220

System.Xml 程序集24

System.Xml assembly 24

被测系统 (SUT) 21

System Under Test (SUT) 21

T

表存储服务46

Table Storage Service 46

TCommand 参数382

TCommand argument 382

TDD(测试驱动开发6、55、182

TDD (Test-Driven Development) 6, 55, 182

时间耦合代码气味109110

Temporal Coupling code smell 109110

可测试性17 , 2123 , 49 , 182

testability 17, 2123, 49, 182

测试双打21 , 24

Test Doubles 21, 24

测试驱动开发 (TDD) 6 , 55 , 182

Test-Driven Development (TDD) 6, 55, 182

测试6623 , 24

testing 6623, 24

TEvent 类型175

TEvent type 175

短信,拦截21

text messages, intercepting 21

第三方插件5

third-party add-ins 5

第三方库386387

third-party libraries 386387

线程池.QueueUserWorkItem 276

ThreadPool.QueueUserWorkItem 276

线程260

threads 260

线程安全的内存存储库257258

thread-safe in-memory Repository 257258

三层图35

three-layer diagram 35

紧耦合代码

tightly coupled code

缺失可组合性分析4750

analysis of missing composability 4750

构建紧密耦合的应用程序3544

building tightly coupled applications 3544

评估紧密耦合的应用程序4447

evaluating tightly coupled applications 4447

TimeProvider 类147 , 148

TimeProvider class 147, 148

时间跨度156

TimeSpan 156

TitledGreeterDecorator 类286

TitledGreeterDecorator class 286

ToEndpointAddress 方法109

ToEndpointAddress method 109

撕裂的生活方式388 , 399

Torn Lifestyles 388, 399

TrackDisposable 方法252 , 254

TrackDisposable method 252, 254

事务方面,使用编译时编织应用349351

transaction aspect, applying using compile-time weaving 349351

事务属性 349 , 350 , 350

TransactionAttribute 349, 350, 350

TransactionCommandServiceDecorator 类321

TransactionCommandServiceDecorator class 321

交易处理,使用抽象应用326327

transaction handling, applying using Abstraction 326327

TransactionScope 类322351443

TransactionScope class 322, 351, 443

短暂的生活方式模式259260 , 439

Transient Lifestyle pattern 259260, 439

瞬态,一次性408 , 479

Transients, disposible 408, 479

传递性93

transitivity 93

尝试语句253

try statement 253

类型实例19

Type instance 19

类型参数175 , 362

type parameters 175, 362

ü

U

UI(用户界面)工具包7

UI (user interface) toolkits 7

用户界面层36

UI Layer 36

UI(用户界面)

UIs (user interfaces)

4546号楼

building 4546

创建层4244

creating layers 4244

可维护,建筑5661

maintainable, building 5661

未经授权访问敏感功能298300

unauthorized access to sensitive functionality 298300

不间断电源 (UPS) 11

uninterrupted power supply (UPS) 11

单价属性225

UnitPrice property 225

单元测试624

unit testing 624

通用Windows编程

Universal Windows Programming

更新货币命令217

UpdateCurrencyCommand 217

更新货币程序

UpdateCurrency program

215216的建筑成分根

building Composition Root of 215216

分层217218

layering in 217218

用214214更新货币

updating currencies with 214214

UpdateHasDiscountsApplied 312

UpdateHasDiscountsApplied 312

UpdateHasTierPricesProperty命令310、312、318 _ _ _

UpdateHasTierPricesProperty command 310, 312, 318

UpdateProductReviewTotals 323

UpdateProductReviewTotals 323

UpdateProductReviewTotalsService 323

UpdateProductReviewTotalsService 323

更新货币214214

updating currencies 214214

UPS(不间断电源)11

UPS (uninterrupted power supply) 11

乌里财产109

Uri property 109

使用扩展方法233

Use extension method 233

userContext 变量198

userContext variable 198

用户界面 (UI) 工具包7

user interface (UI) toolkits 7

用户界面

user interfaces

UserMailAddressChanged 甚至203

UserMailAddressChanged even 203

用户属性39 , 43

User property 39, 43

用户存储库288289

user repository 288289

userRepository 变量198

userRepository variable 198

用户服务类199

UserService class 199

使用语句249

using statement 249

自动注册抽象458

uto-Registered Abstractions 458

UWP(通用 Windows 编程)应用程序87

UWP (Universal Windows Programming) applications 87

V

V

验证方法247

Validate method 247

验证范围 469

validateScopes 469

验证291 , 328

validation 291, 328

验证属性111 , 111

ValidationAttribute 111, 111

valueAccessor 委托415

valueAccessor delegate 415

价值财产270

Value property 270

违规行为

violations

LSP,意外323324

of LSP, accidental 323324

建议零售价

of SRP

195198造成的依赖循环

Dependency cycle caused by 195198

重构以解决依赖循环200203

refactoring from to resolve Dependency Cycle 200203

虚拟代理205 , 272

Virtual Proxy 205, 272

视觉工作室5

Visual Studio 5

挥发性依赖2627 , 50 , 53 , 127 , 355355

Volatile Dependencies 2627, 50, 53, 127, 355355

w

w

警告,抑制447447

warnings, suppressing 447447

WCF(Windows Communication Foundation)247

WCF (Windows Communication Foundation) 247

WcfProductRepository类248、249、294、297 _ _ _ _ _

WcfProductRepository class 248, 249, 294, 297

基于网络的用户界面76

web-based UI 76

欢迎消息生成器151

WelcomeMessageGenerator 151

完成操作222

whenDone action 222

哪里方法401

Where method 401

Windows 通信基础 (WCF) 247

Windows Communication Foundation (WCF) 247

Windows 演示基础 (WPF) 45

Windows Presentation Foundation (WPF) 45

接线

wiring

复合材料422425 , 459461 , 492498

Composites 422425, 459461, 492498

装饰器420422 , 457459 , 489492

Decorators 420422, 457459, 489492

主视图模型225225

MainViewModel 225225

产品管理富客户219225

product-management rich clients 219225

序列417420 , 454457 , 486489

sequences 417420, 454457, 486489

WithParameter 方法412 , 415 , 419 , 425

WithParameter method 412, 415, 419, 425

WPF 应用程序87

WPF applications 87

基于 WPF 的用户界面76

WPF-based UI 76

包装,复合材料177

wrapping, composite 177

写法16、20 _ _

Write method 16, 20

编写器实例14

writer instance 14

写入,与读取314315分开

writes, separating from reads 314315

WrittenMessage 属性24

WrittenMessage property 24

X

X

XML 产品资料库156

XmlProductRepository 156

XmlReader 参数156

XmlReader argument 156

XMLWriter 24

XmlWriter 24

Y

YAGNI 原则55

YAGNI principle 55

图、表和列表列表

Lists of Figures, Tables and Listings